summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/urls/calyx.net.url2
-rw-r--r--app/assets/urls/demo.bitmask.net.url2
-rw-r--r--app/assets/urls/riseup.net.url2
-rw-r--r--app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java9
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java6
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java42
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/EipFragment.java2
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/MainActivity.java12
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/Provider.java35
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java1
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java41
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderCredentialsBaseActivity.java18
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderListBaseActivity.java3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderManager.java53
-rw-r--r--app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java10
-rw-r--r--app/src/production/java/se/leap/bitmaskclient/ProviderListActivity.java3
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/ProviderTest.java30
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java15
-rw-r--r--app/src/test/resources/externalDir/leapcolombia.json3
-rw-r--r--app/src/test/resources/preconfigured/calyx.net.json37
-rw-r--r--app/src/test/resources/preconfigured/calyx.net.pem31
-rw-r--r--app/src/test/resources/preconfigured/demo.bitmask.net.json42
-rw-r--r--app/src/test/resources/preconfigured/demo.bitmask.net.pem32
-rw-r--r--app/src/test/resources/preconfigured/riseup.net.json37
-rw-r--r--app/src/test/resources/preconfigured/riseup.net.pem32
-rw-r--r--app/src/test/resources/preconfigured/urls/calyx.net.url3
-rw-r--r--app/src/test/resources/preconfigured/urls/demo.bitmask.net.url3
-rw-r--r--app/src/test/resources/preconfigured/urls/riseup.net.url3
m---------ics-openvpn0
29 files changed, 388 insertions, 121 deletions
diff --git a/app/assets/urls/calyx.net.url b/app/assets/urls/calyx.net.url
index 8de04fe9..807e9e18 100644
--- a/app/assets/urls/calyx.net.url
+++ b/app/assets/urls/calyx.net.url
@@ -1,3 +1,3 @@
{
- "main_url" : "https://calyx.net/"
+ "main_url" : "https://calyx.net"
}
diff --git a/app/assets/urls/demo.bitmask.net.url b/app/assets/urls/demo.bitmask.net.url
index 1a412055..0c4de648 100644
--- a/app/assets/urls/demo.bitmask.net.url
+++ b/app/assets/urls/demo.bitmask.net.url
@@ -1,3 +1,3 @@
{
- "main_url" : "https://demo.bitmask.net/"
+ "main_url" : "https://demo.bitmask.net"
}
diff --git a/app/assets/urls/riseup.net.url b/app/assets/urls/riseup.net.url
index 4548b433..42cdb979 100644
--- a/app/assets/urls/riseup.net.url
+++ b/app/assets/urls/riseup.net.url
@@ -1,3 +1,3 @@
{
- "main_url" : "https://riseup.net/"
+ "main_url" : "https://riseup.net"
}
diff --git a/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java b/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java
index 42bd576a..798b6433 100644
--- a/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java
+++ b/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java
@@ -140,7 +140,7 @@ public class ProviderApiManager extends ProviderApiManagerBase {
else
providerDotJsonString = downloadFromApiUrlWithProviderCA("/provider.json", caCert, providerDefinition, dangerOn);
- if (!isValidJson(providerDotJsonString)) {
+ if (ConfigHelper.checkErroneousDownload(providerDotJsonString) || !isValidJson(providerDotJsonString)) {
result.putString(ERRORS, resources.getString(malformed_url));
result.putBoolean(BROADCAST_RESULT_KEY, false);
return result;
@@ -206,9 +206,7 @@ public class ProviderApiManager extends ProviderApiManagerBase {
protected Bundle updateVpnCertificate(Provider provider) {
Bundle result = new Bundle();
try {
- JSONObject providerJson = provider.getDefinition();
- String providerMainUrl = providerJson.getString(Provider.API_URL);
- URL newCertStringUrl = new URL(providerMainUrl + "/" + providerJson.getString(Provider.API_VERSION) + "/" + PROVIDER_VPN_CERTIFICATE);
+ URL newCertStringUrl = new URL(provider.getApiUrlWithVersion() + "/" + PROVIDER_VPN_CERTIFICATE);
String certString = downloadWithProviderCA(provider.getCaCert(), newCertStringUrl.toString(), lastDangerOn);
if (ConfigHelper.checkErroneousDownload(certString)) {
@@ -223,8 +221,7 @@ public class ProviderApiManager extends ProviderApiManagerBase {
}
}
result = loadCertificate(provider, certString);
- } catch (IOException | JSONException e) {
- // TODO try to get Provider Json
+ } catch (IOException e) {
setErrorResult(result, downloading_vpn_certificate_failed, null);
e.printStackTrace();
}
diff --git a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java
index 6c312c87..f701b7aa 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java
@@ -913,7 +913,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
// CONNECTED
// Does not work :(
notificationManager.buildOpenVpnNotification(
- mProfile.mName,
+ mProfile != null ? mProfile.mName : "",
VpnStatus.getLastCleanLogMessage(this),
VpnStatus.getLastCleanLogMessage(this),
level,
@@ -944,7 +944,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
humanReadableByteCount(out, false, getResources()),
humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true, getResources()));
notificationManager.buildOpenVpnNotification(
- mProfile.mName,
+ mProfile != null ? mProfile.mName : "",
netstat,
null,
LEVEL_CONNECTED,
@@ -987,7 +987,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
public void requestInputFromUser(int resid, String needed) {
VpnStatus.updateStateString("NEED", "need " + needed, resid, LEVEL_WAITING_FOR_USER_INPUT);
notificationManager.buildOpenVpnNotification(
- mProfile.mName,
+ mProfile != null ? mProfile.mName : "",
getString(resid),
getString(resid),
LEVEL_WAITING_FOR_USER_INPUT,
diff --git a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
index a52df460..a21a9601 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
@@ -27,6 +27,8 @@ import org.spongycastle.util.encoders.Base64;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -47,9 +49,11 @@ import java.security.interfaces.RSAPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import static android.R.attr.name;
import static se.leap.bitmaskclient.Constants.PREFERENCES_APP_VERSION;
@@ -74,7 +78,7 @@ public class ConfigHelper {
public static boolean checkErroneousDownload(String downloadedString) {
try {
- if (downloadedString == null || downloadedString.isEmpty() || new JSONObject(downloadedString).has(ProviderAPI.ERRORS)) {
+ if (downloadedString == null || downloadedString.isEmpty() || new JSONObject(downloadedString).has(ProviderAPI.ERRORS) || new JSONObject(downloadedString).has(ProviderAPI.BACKEND_ERROR_KEY)) {
return true;
} else {
return false;
@@ -125,36 +129,14 @@ public class ConfigHelper {
return (X509Certificate) certificate;
}
+ public static String loadInputStreamAsString(java.io.InputStream is) {
+ java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A");
+ return s.hasNext() ? s.next() : "";
+ }
- public static String loadInputStreamAsString(InputStream inputStream) {
- BufferedReader in = null;
- try {
- StringBuilder buf = new StringBuilder();
- in = new BufferedReader(new InputStreamReader(inputStream));
-
- String str;
- boolean isFirst = true;
- while ( (str = in.readLine()) != null ) {
- if (isFirst)
- isFirst = false;
- else
- buf.append('\n');
- buf.append(str);
- }
- return buf.toString();
- } catch (IOException e) {
- Log.e(TAG, "Error opening asset " + name);
- } finally {
- if (in != null) {
- try {
- in.close();
- } catch (IOException e) {
- Log.e(TAG, "Error closing asset " + name);
- }
- }
- }
-
- return null;
+ //allows us to mock FileInputStream
+ public static InputStream getInputStreamFrom(String filePath) throws FileNotFoundException {
+ return new FileInputStream(filePath);
}
protected static RSAPrivateKey parseRsaKeyFromString(String rsaKeyString) {
diff --git a/app/src/main/java/se/leap/bitmaskclient/EipFragment.java b/app/src/main/java/se/leap/bitmaskclient/EipFragment.java
index fb57aea8..8d1fa03a 100644
--- a/app/src/main/java/se/leap/bitmaskclient/EipFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/EipFragment.java
@@ -60,7 +60,7 @@ import static se.leap.bitmaskclient.Constants.REQUEST_CODE_LOG_IN;
import static se.leap.bitmaskclient.Constants.REQUEST_CODE_SWITCH_PROVIDER;
import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES;
import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE;
-import static se.leap.bitmaskclient.ProviderCredentialsBaseActivity.USER_MESSAGE;
+import static se.leap.bitmaskclient.ProviderAPI.USER_MESSAGE;
import static se.leap.bitmaskclient.R.string.vpn_certificate_user_message;
public class EipFragment extends Fragment implements Observer {
diff --git a/app/src/main/java/se/leap/bitmaskclient/MainActivity.java b/app/src/main/java/se/leap/bitmaskclient/MainActivity.java
index 6e778309..b4be55a4 100644
--- a/app/src/main/java/se/leap/bitmaskclient/MainActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/MainActivity.java
@@ -20,7 +20,6 @@ import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
-import org.jetbrains.annotations.NotNull;
import org.json.JSONException;
import org.json.JSONObject;
@@ -58,7 +57,7 @@ import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFI
import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE;
import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
-import static se.leap.bitmaskclient.ProviderCredentialsBaseActivity.USER_MESSAGE;
+import static se.leap.bitmaskclient.ProviderAPI.USER_MESSAGE;
import static se.leap.bitmaskclient.R.string.downloading_vpn_certificate_failed;
import static se.leap.bitmaskclient.R.string.vpn_certificate_user_message;
@@ -67,12 +66,7 @@ public class MainActivity extends AppCompatActivity implements Observer {
public final static String TAG = MainActivity.class.getSimpleName();
- private static final String KEY_ACTIVITY_STATE = "key state of activity";
- private static final String DEFAULT_UI_STATE = "default state";
- private static final String SHOW_DIALOG_STATE = "show dialog";
- private static final String REASON_TO_FAIL = "reason to fail";
-
- private static Provider provider = new Provider();
+ private Provider provider = new Provider();
private SharedPreferences preferences;
private EipStatus eipStatus;
private NavigationDrawerFragment navigationDrawerFragment;
@@ -198,7 +192,7 @@ public class MainActivity extends AppCompatActivity implements Observer {
break;
}
}
-
+ //TODO: Why do we want this --v? legacy and redundant?
Fragment fragment = new EipFragment();
Bundle arguments = new Bundle();
arguments.putParcelable(PROVIDER_KEY, provider);
diff --git a/app/src/main/java/se/leap/bitmaskclient/Provider.java b/app/src/main/java/se/leap/bitmaskclient/Provider.java
index 7104143c..fd067bf9 100644
--- a/app/src/main/java/se/leap/bitmaskclient/Provider.java
+++ b/app/src/main/java/se/leap/bitmaskclient/Provider.java
@@ -90,9 +90,8 @@ public final class Provider implements Parcelable {
}
if (definition != null) {
try {
- this.definition = new JSONObject(definition);
- parseDefinition(this.definition);
- } catch (JSONException | NullPointerException e) {
+ define(new JSONObject(definition));
+ } catch (JSONException e) {
e.printStackTrace();
}
}
@@ -133,26 +132,8 @@ public final class Provider implements Parcelable {
}
public boolean define(JSONObject providerJson) {
- /*
- * fix against "api_uri": "https://calyx.net.malicious.url.net:4430",
- * This method aims to prevent attacks where the provider.json file got manipulated by a third party.
- * The main url should not change.
- */
-
- try {
- String providerApiUrl = providerJson.getString(Provider.API_URL);
- String providerDomain = providerJson.getString(Provider.DOMAIN);
- if (getMainUrlString().contains(providerDomain) && providerApiUrl.contains(providerDomain + ":")) {
- definition = providerJson;
- parseDefinition(definition);
- return true;
- } else {
- return false;
- }
- } catch (JSONException e) {
- e.printStackTrace();
- return false;
- }
+ definition = providerJson;
+ return parseDefinition(definition);
}
public JSONObject getDefinition() {
@@ -297,8 +278,6 @@ public final class Provider implements Parcelable {
try {
json.put(Provider.MAIN_URL, mainUrl);
//TODO: add other fields here?
- //this is used to save custom providers as json. I guess this doesn't work correctly
- //TODO 2: verify that
} catch (JSONException e) {
e.printStackTrace();
}
@@ -345,7 +324,7 @@ public final class Provider implements Parcelable {
}
}
- private void parseDefinition(JSONObject definition) {
+ private boolean parseDefinition(JSONObject definition) {
try {
String pin = definition.getString(CA_CERT_FINGERPRINT);
this.certificatePin = pin.split(":")[1].trim();
@@ -354,8 +333,9 @@ public final class Provider implements Parcelable {
this.allowAnonymous = definition.getJSONObject(Provider.SERVICE).getBoolean(PROVIDER_ALLOW_ANONYMOUS);
this.allowRegistered = definition.getJSONObject(Provider.SERVICE).getBoolean(PROVIDER_ALLOWED_REGISTERED);
this.apiVersion = getDefinition().getString(Provider.API_VERSION);
+ return true;
} catch (JSONException | ArrayIndexOutOfBoundsException | MalformedURLException e) {
- e.printStackTrace();
+ return false;
}
}
@@ -446,5 +426,4 @@ public final class Provider implements Parcelable {
allowRegistered = false;
allowAnonymous = false;
}
-
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java
index f5efde05..f1f474d7 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java
@@ -51,6 +51,7 @@ public class ProviderAPI extends IntentService implements ProviderApiManagerBase
ERRORID = "errorId",
BACKEND_ERROR_KEY = "error",
BACKEND_ERROR_MESSAGE = "message",
+ USER_MESSAGE = "userMessage",
DOWNLOAD_SERVICE_JSON = "ProviderAPI.DOWNLOAD_SERVICE_JSON",
PROVIDER_SET_UP = "ProviderAPI.PROVIDER_SET_UP";
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java
index b93abaeb..2cde431e 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java
@@ -46,6 +46,7 @@ import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.util.ArrayList;
import java.util.List;
+import java.util.NoSuchElementException;
import javax.net.ssl.SSLHandshakeException;
@@ -63,19 +64,16 @@ import static se.leap.bitmaskclient.Constants.PROVIDER_PRIVATE_KEY;
import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.ProviderAPI.BACKEND_ERROR_KEY;
import static se.leap.bitmaskclient.ProviderAPI.BACKEND_ERROR_MESSAGE;
-import static se.leap.bitmaskclient.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING;
-import static se.leap.bitmaskclient.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON;
-import static se.leap.bitmaskclient.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_INVALID_CERTIFICATE;
-import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_EIP_SERVICE;
-import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_SERVICE_JSON;
+import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.ProviderAPI.ERRORID;
import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
import static se.leap.bitmaskclient.ProviderAPI.FAILED_LOGIN;
import static se.leap.bitmaskclient.ProviderAPI.FAILED_SIGNUP;
-import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE;
+import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.ProviderAPI.LOGOUT_FAILED;
import static se.leap.bitmaskclient.ProviderAPI.LOG_IN;
import static se.leap.bitmaskclient.ProviderAPI.LOG_OUT;
@@ -90,15 +88,18 @@ import static se.leap.bitmaskclient.ProviderAPI.SUCCESSFUL_LOGIN;
import static se.leap.bitmaskclient.ProviderAPI.SUCCESSFUL_LOGOUT;
import static se.leap.bitmaskclient.ProviderAPI.SUCCESSFUL_SIGNUP;
import static se.leap.bitmaskclient.ProviderAPI.UPDATE_PROVIDER_DETAILS;
+import static se.leap.bitmaskclient.ProviderAPI.USER_MESSAGE;
+import static se.leap.bitmaskclient.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING;
+import static se.leap.bitmaskclient.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON;
+import static se.leap.bitmaskclient.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_INVALID_CERTIFICATE;
import static se.leap.bitmaskclient.R.string.certificate_error;
-import static se.leap.bitmaskclient.R.string.switch_provider_menu_option;
-import static se.leap.bitmaskclient.R.string.vpn_certificate_is_invalid;
import static se.leap.bitmaskclient.R.string.error_io_exception_user_message;
import static se.leap.bitmaskclient.R.string.error_json_exception_user_message;
import static se.leap.bitmaskclient.R.string.error_no_such_algorithm_exception_user_message;
import static se.leap.bitmaskclient.R.string.malformed_url;
import static se.leap.bitmaskclient.R.string.server_unreachable_message;
import static se.leap.bitmaskclient.R.string.service_is_down_error;
+import static se.leap.bitmaskclient.R.string.vpn_certificate_is_invalid;
import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_cert;
import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_details;
import static se.leap.bitmaskclient.R.string.warning_expired_provider_cert;
@@ -290,7 +291,7 @@ public abstract class ProviderApiManagerBase {
JSONObject stepResult = null;
OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), stepResult);
if (okHttpClient == null) {
- return authFailedNotification(stepResult, username);
+ return backendErrorNotification(stepResult, username);
}
LeapSRPSession client = new LeapSRPSession(username, password);
@@ -302,7 +303,7 @@ public abstract class ProviderApiManagerBase {
Bundle result = new Bundle();
if (api_result.has(ERRORS) || api_result.has(BACKEND_ERROR_KEY))
- result = authFailedNotification(api_result, username);
+ result = backendErrorNotification(api_result, username);
else {
result.putString(CREDENTIALS_USERNAME, username);
result.putString(CREDENTIALS_PASSWORD, password);
@@ -349,7 +350,7 @@ public abstract class ProviderApiManagerBase {
OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), stepResult);
if (okHttpClient == null) {
- return authFailedNotification(stepResult, username);
+ return backendErrorNotification(stepResult, username);
}
LeapSRPSession client = new LeapSRPSession(username, password);
@@ -367,15 +368,15 @@ public abstract class ProviderApiManagerBase {
if (client.verify(M2)) {
result.putBoolean(BROADCAST_RESULT_KEY, true);
} else {
- authFailedNotification(step_result, username);
+ backendErrorNotification(step_result, username);
}
} else {
result.putBoolean(BROADCAST_RESULT_KEY, false);
result.putString(CREDENTIALS_USERNAME, username);
- result.putString(resources.getString(R.string.user_message), resources.getString(R.string.error_srp_math_error_user_message));
+ result.putString(USER_MESSAGE, resources.getString(R.string.error_srp_math_error_user_message));
}
} catch (JSONException e) {
- result = authFailedNotification(step_result, username);
+ result = backendErrorNotification(step_result, username);
e.printStackTrace();
}
@@ -391,7 +392,7 @@ public abstract class ProviderApiManagerBase {
return true;
}
- private Bundle authFailedNotification(JSONObject result, String username) {
+ private Bundle backendErrorNotification(JSONObject result, String username) {
Bundle userNotificationBundle = new Bundle();
if (result.has(ERRORS)) {
Object baseErrorMessage = result.opt(ERRORS);
@@ -400,14 +401,14 @@ public abstract class ProviderApiManagerBase {
JSONObject errorMessage = result.getJSONObject(ERRORS);
String errorType = errorMessage.keys().next().toString();
String message = errorMessage.get(errorType).toString();
- userNotificationBundle.putString(resources.getString(R.string.user_message), message);
- } catch (JSONException e) {
+ userNotificationBundle.putString(USER_MESSAGE, message);
+ } catch (JSONException | NoSuchElementException | NullPointerException e) {
e.printStackTrace();
}
} else if (baseErrorMessage instanceof String) {
try {
String errorMessage = result.getString(ERRORS);
- userNotificationBundle.putString(resources.getString(R.string.user_message), errorMessage);
+ userNotificationBundle.putString(USER_MESSAGE, errorMessage);
} catch (JSONException e) {
e.printStackTrace();
}
@@ -418,7 +419,7 @@ public abstract class ProviderApiManagerBase {
if (result.has(BACKEND_ERROR_MESSAGE)) {
backendErrorMessage = resources.getString(R.string.error) + result.getString(BACKEND_ERROR_MESSAGE);
}
- userNotificationBundle.putString(resources.getString(R.string.user_message), backendErrorMessage);
+ userNotificationBundle.putString(USER_MESSAGE, backendErrorMessage);
} catch (JSONException e) {
e.printStackTrace();
}
@@ -431,7 +432,7 @@ public abstract class ProviderApiManagerBase {
return userNotificationBundle;
}
- void sendToReceiverOrBroadcast(ResultReceiver receiver, int resultCode, Bundle resultData, Provider provider) {
+ private void sendToReceiverOrBroadcast(ResultReceiver receiver, int resultCode, Bundle resultData, Provider provider) {
if (resultData == null || resultData == Bundle.EMPTY) {
resultData = new Bundle();
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderCredentialsBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/ProviderCredentialsBaseActivity.java
index d41be512..e6877756 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderCredentialsBaseActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderCredentialsBaseActivity.java
@@ -30,6 +30,7 @@ import org.json.JSONException;
import butterknife.InjectView;
import butterknife.OnClick;
import se.leap.bitmaskclient.Constants.CREDENTIAL_ERRORS;
+import se.leap.bitmaskclient.eip.EipCommand;
import se.leap.bitmaskclient.userstatus.User;
import static android.view.View.GONE;
@@ -41,9 +42,12 @@ import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY;
import static se.leap.bitmaskclient.Constants.CREDENTIALS_PASSWORD;
import static se.leap.bitmaskclient.Constants.CREDENTIALS_USERNAME;
import static se.leap.bitmaskclient.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.ProviderAPI.BACKEND_ERROR_KEY;
import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
import static se.leap.bitmaskclient.ProviderAPI.LOG_IN;
import static se.leap.bitmaskclient.ProviderAPI.SIGN_UP;
+import static se.leap.bitmaskclient.ProviderAPI.USER_MESSAGE;
/**
* Base Activity for activities concerning a provider interaction
@@ -59,7 +63,6 @@ public abstract class ProviderCredentialsBaseActivity extends ConfigWizardBaseAc
final private static String SHOWING_FORM = "SHOWING_FORM";
final private static String PERFORMING_ACTION = "PERFORMING_ACTION";
- final public static String USER_MESSAGE = "USER_MESSAGE";
final private static String USERNAME_ERROR = "USERNAME_ERROR";
final private static String PASSWORD_ERROR = "PASSWORD_ERROR";
final private static String PASSWORD_VERIFICATION_ERROR = "PASSWORD_VERIFICATION_ERROR";
@@ -344,8 +347,8 @@ public abstract class ProviderCredentialsBaseActivity extends ConfigWizardBaseAc
if (arguments.containsKey(CREDENTIAL_ERRORS.USERNAME_MISSING.toString())) {
usernameError.setError(getString(R.string.username_ask));
}
- if (arguments.containsKey(getString(R.string.user_message))) {
- String userMessageString = arguments.getString(getString(R.string.user_message));
+ if (arguments.containsKey(USER_MESSAGE)) {
+ String userMessageString = arguments.getString(USER_MESSAGE);
try {
userMessageString = new JSONArray(userMessageString).getString(0);
} catch (JSONException e) {
@@ -395,6 +398,10 @@ public abstract class ProviderCredentialsBaseActivity extends ConfigWizardBaseAc
switch (resultCode) {
case ProviderAPI.SUCCESSFUL_SIGNUP:
+ String password = resultData.getString(CREDENTIALS_PASSWORD);
+ String username = resultData.getString(CREDENTIALS_USERNAME);
+ login(username, password);
+ break;
case ProviderAPI.SUCCESSFUL_LOGIN:
downloadVpnCertificate(handledProvider);
break;
@@ -403,12 +410,11 @@ public abstract class ProviderCredentialsBaseActivity extends ConfigWizardBaseAc
handleReceivedErrors((Bundle) intent.getParcelableExtra(BROADCAST_RESULT_KEY));
break;
+ case ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
+ // error handling takes place in MainActivity
case ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
successfullyFinished(handledProvider);
break;
- case ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
- // TODO activity.setResult(RESULT_CANCELED);
- break;
}
}
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderListBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/ProviderListBaseActivity.java
index e961b0a2..3bf51a8c 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderListBaseActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderListBaseActivity.java
@@ -207,7 +207,8 @@ public abstract class ProviderListBaseActivity extends ConfigWizardBaseActivity
void handleProviderSetUp(Provider handledProvider) {
this.provider = handledProvider;
-
+ adapter.add(provider);
+ adapter.saveProviders();
if (provider.allowsAnonymous()) {
mConfigState.putExtra(SERVICES_RETRIEVED, true);
downloadVpnCertificate();
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java b/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java
index ed41be67..97ba3b98 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java
@@ -31,6 +31,8 @@ public class ProviderManager implements AdapteeCollection<Provider> {
private File externalFilesDir;
private Set<Provider> defaultProviders;
private Set<Provider> customProviders;
+ private Set<URL> defaultProviderURLs;
+ private Set<URL> customProviderURLs;
private static ProviderManager instance;
@@ -52,11 +54,20 @@ public class ProviderManager implements AdapteeCollection<Provider> {
private void addDefaultProviders(AssetManager assets_manager) {
try {
defaultProviders = providersFromAssets(URLS, assets_manager.list(URLS));
+ defaultProviderURLs = getProviderUrlSetFromProviderSet(defaultProviders);
} catch (IOException e) {
e.printStackTrace();
}
}
+ private Set<URL> getProviderUrlSetFromProviderSet(Set<Provider> providers) {
+ HashSet<URL> providerUrls = new HashSet<>();
+ for (Provider provider : providers) {
+ providerUrls.add(provider.getMainUrl().getUrl());
+ }
+ return providerUrls;
+ }
+
private Set<Provider> providersFromAssets(String directory, String[] relativeFilePaths) {
Set<Provider> providers = new HashSet<>();
@@ -89,13 +100,14 @@ public class ProviderManager implements AdapteeCollection<Provider> {
customProviders = externalFilesDir != null && externalFilesDir.isDirectory() ?
providersFromFiles(externalFilesDir.list()) :
new HashSet<Provider>();
+ customProviderURLs = getProviderUrlSetFromProviderSet(customProviders);
}
private Set<Provider> providersFromFiles(String[] files) {
Set<Provider> providers = new HashSet<>();
try {
for (String file : files) {
- String mainUrl = extractMainUrlFromInputStream(new FileInputStream(externalFilesDir.getAbsolutePath() + "/" + file));
+ String mainUrl = extractMainUrlFromInputStream(ConfigHelper.getInputStreamFrom(externalFilesDir.getAbsolutePath() + "/" + file));
providers.add(new Provider(new URL(mainUrl)));
}
} catch (MalformedURLException | FileNotFoundException e) {
@@ -132,6 +144,8 @@ public class ProviderManager implements AdapteeCollection<Provider> {
allProviders.addAll(defaultProviders);
if(customProviders != null)
allProviders.addAll(customProviders);
+ //add an option to add a custom provider
+ //TODO: refactor me?
allProviders.add(new Provider());
return allProviders;
}
@@ -153,32 +167,59 @@ public class ProviderManager implements AdapteeCollection<Provider> {
@Override
public boolean add(Provider element) {
- return !defaultProviders.contains(element) || customProviders.add(element);
+ return element != null &&
+ !defaultProviderURLs.contains(element.getMainUrl().getUrl()) &&
+ customProviders.add(element) &&
+ customProviderURLs.add(element.getMainUrl().getUrl());
}
@Override
public boolean remove(Object element) {
- return customProviders.remove(element);
+ return element instanceof Provider &&
+ customProviders.remove(element) &&
+ customProviderURLs.remove(((Provider) element).getMainUrl().getUrl());
}
@Override
public boolean addAll(Collection<? extends Provider> elements) {
- return customProviders.addAll(elements);
+ Iterator iterator = elements.iterator();
+ boolean addedAll = true;
+ while (iterator.hasNext()) {
+ Provider p = (Provider) iterator.next();
+ addedAll = customProviders.add(p) &&
+ customProviderURLs.add(p.getMainUrl().getUrl()) &&
+ addedAll;
+ }
+ return addedAll;
}
@Override
public boolean removeAll(Collection<?> elements) {
- if(!elements.getClass().equals(Provider.class))
+ Iterator iterator = elements.iterator();
+ boolean removedAll = true;
+ try {
+ while (iterator.hasNext()) {
+ Provider p = (Provider) iterator.next();
+ removedAll = ((defaultProviders.remove(p) && defaultProviderURLs.remove(p.getMainUrl().getUrl())) ||
+ (customProviders.remove(p) && customProviderURLs.remove(p.getMainUrl().getUrl()))) &&
+ removedAll;
+ }
+ } catch (ClassCastException e) {
return false;
- return defaultProviders.removeAll(elements) || customProviders.removeAll(elements);
+ }
+
+ return removedAll;
}
@Override
public void clear() {
defaultProviders.clear();
customProviders.clear();
+ customProviderURLs.clear();
+ defaultProviderURLs.clear();
}
+ //FIXME: removed custom providers should be deleted here as well
void saveCustomProvidersToFile() {
try {
for (Provider provider : customProviders) {
diff --git a/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java b/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java
index e5ee6c49..885d69db 100644
--- a/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java
+++ b/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java
@@ -34,6 +34,7 @@ import se.leap.bitmaskclient.eip.EIP;
import static android.text.TextUtils.isEmpty;
import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_KEY;
+import static se.leap.bitmaskclient.Constants.PROVIDER_KEY;
import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
import static se.leap.bitmaskclient.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING;
@@ -84,6 +85,7 @@ public class ProviderApiManager extends ProviderApiManagerBase {
//provider details invalid
if (currentDownload.containsKey(ERRORS)) {
+ currentDownload.putParcelable(PROVIDER_KEY, provider);
return currentDownload;
}
@@ -121,7 +123,7 @@ public class ProviderApiManager extends ProviderApiManagerBase {
providerDotJsonString = downloadFromApiUrlWithProviderCA("/provider.json", caCert, providerDefinition);
}
- if (!isValidJson(providerDotJsonString)) {
+ if (ConfigHelper.checkErroneousDownload(providerDotJsonString) || !isValidJson(providerDotJsonString)) {
setErrorResult(result, malformed_url, null);
return result;
}
@@ -177,9 +179,7 @@ public class ProviderApiManager extends ProviderApiManagerBase {
protected Bundle updateVpnCertificate(Provider provider) {
Bundle result = new Bundle();
try {
- JSONObject providerJson = provider.getDefinition();
- String providerMainUrl = providerJson.getString(Provider.API_URL);
- URL newCertStringUrl = new URL(providerMainUrl + "/" + providerJson.getString(Provider.API_VERSION) + "/" + PROVIDER_VPN_CERTIFICATE);
+ URL newCertStringUrl = new URL(provider.getApiUrlWithVersion() + "/" + PROVIDER_VPN_CERTIFICATE);
String certString = downloadWithProviderCA(provider.getCaCert(), newCertStringUrl.toString());
if (ConfigHelper.checkErroneousDownload(certString)) {
@@ -194,7 +194,7 @@ public class ProviderApiManager extends ProviderApiManagerBase {
}
}
return loadCertificate(provider, certString);
- } catch (IOException | JSONException e) {
+ } catch (IOException e) {
// TODO try to get Provider Json
setErrorResult(result, downloading_vpn_certificate_failed, null);
e.printStackTrace();
diff --git a/app/src/production/java/se/leap/bitmaskclient/ProviderListActivity.java b/app/src/production/java/se/leap/bitmaskclient/ProviderListActivity.java
index b6e67331..cf1d1aa6 100644
--- a/app/src/production/java/se/leap/bitmaskclient/ProviderListActivity.java
+++ b/app/src/production/java/se/leap/bitmaskclient/ProviderListActivity.java
@@ -39,11 +39,10 @@ public class ProviderListActivity extends ProviderListBaseActivity {
setUpProvider();
}
+ @Override
public void showAndSelectProvider(String provider_main_url) {
try {
provider = new Provider(new URL((provider_main_url)));
- adapter.add(provider);
- adapter.saveProviders();
autoSelectProvider(provider);
} catch (MalformedURLException e) {
e.printStackTrace();
diff --git a/app/src/test/java/se/leap/bitmaskclient/ProviderTest.java b/app/src/test/java/se/leap/bitmaskclient/ProviderTest.java
index 794c3087..495d5b3f 100644
--- a/app/src/test/java/se/leap/bitmaskclient/ProviderTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/ProviderTest.java
@@ -1,9 +1,15 @@
package se.leap.bitmaskclient;
+import org.json.JSONException;
import org.junit.Test;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
import se.leap.bitmaskclient.testutils.TestSetupHelper;
+import static junit.framework.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
@@ -18,4 +24,28 @@ public class ProviderTest {
assertTrue("Providers should be same:", p1.equals(p2));
}
+ @Test
+ public void testEquals_sameFields_returnsFalse() throws Exception {
+ Provider p1 = TestSetupHelper.getConfiguredProvider();
+ Provider p2 = TestSetupHelper.getConfiguredProvider();
+ p2.setMainUrl("http://somethingsdiffer.org");
+ assertFalse("Providers should be same:", p1.equals(p2));
+ }
+
+ // see ProviderManagerTest testing add(...)
+ @Test
+ public void testEqualsThroughSetContains_differentFields_returnsFalse() throws Exception {
+ Provider p1 = TestSetupHelper.getConfiguredProvider();
+ Provider p2 = TestSetupHelper.getConfiguredProvider();
+ p2.setMainUrl("http://somethingsdiffer.org");
+ Provider p3 = new Provider("https://anotherprovider.net");
+
+ Set<Provider> defaultProviders = new HashSet<>();
+ defaultProviders.add(p1);
+ defaultProviders.add(p2);
+
+ assertTrue(defaultProviders.contains(p1));
+ assertTrue(defaultProviders.contains(p2));
+ assertFalse(defaultProviders.contains(p3));
+ }
}
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java b/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java
index c2362c7b..d85b050f 100644
--- a/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java
@@ -14,6 +14,7 @@ import org.json.JSONObject;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.NoSuchAlgorithmException;
@@ -29,7 +30,6 @@ import java.util.Set;
import okhttp3.OkHttpClient;
import se.leap.bitmaskclient.ConfigHelper;
-import se.leap.bitmaskclient.Constants;
import se.leap.bitmaskclient.OkHttpClientGenerator;
import se.leap.bitmaskclient.Provider;
import se.leap.bitmaskclient.R;
@@ -343,6 +343,18 @@ public class MockHelper {
return resultReceiver;
}
+ public static void mockConfigHelperForFileInputStream() throws FileNotFoundException {
+ mockStatic(ConfigHelper.class);
+ when(ConfigHelper.loadInputStreamAsString(any(InputStream.class))).thenCallRealMethod();
+ when(ConfigHelper.getInputStreamFrom(anyString())).thenAnswer(new Answer<InputStream>() {
+ @Override
+ public InputStream answer(InvocationOnMock invocation) throws Throwable {
+ String filename = (String) invocation.getArguments()[0];
+ return getClass().getClassLoader().getResourceAsStream(filename);
+ }
+ });
+ }
+
public static void mockConfigHelper(String mockedFingerprint, final Provider providerFromPrefs) throws CertificateEncodingException, NoSuchAlgorithmException {
// FIXME use MockSharedPreferences instead of provider
mockStatic(ConfigHelper.class);
@@ -366,6 +378,7 @@ public class MockHelper {
when(ConfigHelper.getFingerprintFromCertificate(any(X509Certificate.class), anyString())).thenReturn(mockedFingerprint);
when(ConfigHelper.checkErroneousDownload(anyString())).thenCallRealMethod();
when(ConfigHelper.parseX509CertificateFromString(anyString())).thenCallRealMethod();
+ when(ConfigHelper.loadInputStreamAsString(any(InputStream.class))).thenCallRealMethod();
}
public static void mockFingerprintForCertificate(String mockedFingerprint) throws CertificateEncodingException, NoSuchAlgorithmException {
mockStatic(ConfigHelper.class);
diff --git a/app/src/test/resources/externalDir/leapcolombia.json b/app/src/test/resources/externalDir/leapcolombia.json
new file mode 100644
index 00000000..6820988c
--- /dev/null
+++ b/app/src/test/resources/externalDir/leapcolombia.json
@@ -0,0 +1,3 @@
+{
+ "main_url" : "https://leapcolombia.org"
+} \ No newline at end of file
diff --git a/app/src/test/resources/preconfigured/calyx.net.json b/app/src/test/resources/preconfigured/calyx.net.json
new file mode 100644
index 00000000..30ab43c5
--- /dev/null
+++ b/app/src/test/resources/preconfigured/calyx.net.json
@@ -0,0 +1,37 @@
+{
+ "api_uri": "https://calyx.net:4430",
+ "api_version": "1",
+ "ca_cert_fingerprint": "SHA256: 43683c9ba3862c5384a8c1885072fcac40b5d2d4dd67331443f13a3077fa2e69",
+ "ca_cert_uri": "https://calyx.net/ca.crt",
+ "default_language": "en",
+ "description": {
+ "en": "Calyx Institute privacy focused ISP testbed"
+ },
+ "domain": "calyx.net",
+ "enrollment_policy": "open",
+ "languages": [
+ "en"
+ ],
+ "name": {
+ "en": "calyx"
+ },
+ "service": {
+ "allow_anonymous": false,
+ "allow_free": true,
+ "allow_limited_bandwidth": false,
+ "allow_paid": false,
+ "allow_registration": true,
+ "allow_unlimited_bandwidth": true,
+ "bandwidth_limit": 102400,
+ "default_service_level": 1,
+ "levels": {
+ "1": {
+ "description": "Please donate.",
+ "name": "free"
+ }
+ }
+ },
+ "services": [
+ "openvpn"
+ ]
+} \ No newline at end of file
diff --git a/app/src/test/resources/preconfigured/calyx.net.pem b/app/src/test/resources/preconfigured/calyx.net.pem
new file mode 100644
index 00000000..cedb2e38
--- /dev/null
+++ b/app/src/test/resources/preconfigured/calyx.net.pem
@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIFYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQ0FADBEMQ4wDAYDVQQKDAVjYWx5
+eDEaMBgGA1UECwwRaHR0cHM6Ly9jYWx5eC5uZXQxFjAUBgNVBAMMDWNhbHl4IFJv
+b3QgQ0EwHhcNMTMwNzAyMDAwMDAwWhcNMjMwNzAyMDAwMDAwWjBEMQ4wDAYDVQQK
+DAVjYWx5eDEaMBgGA1UECwwRaHR0cHM6Ly9jYWx5eC5uZXQxFjAUBgNVBAMMDWNh
+bHl4IFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDupdnx
+Bgat537XOqrZOulE/RvjoXB1S07sy9/MMtksXFoQuWJZRCSTp1Jaqg3H/e9o1nct
+LQO91+izfJe07TUyajFl7CfllYgMeyKTYcT85dFwNX4pcIHZr8UpmO0MpGBoR4W1
+8cPa3vxAG0CsyUmrASJVyhRouk4qazRosM5RwBxTdMzCK7L3SwqPQoxlY9YmRJlD
+XYZlK5VMJd0dj9XxhMeFs5n43R0bsDENryrExSbuxoNfnUoQg3wffKk+Z0gW7YgW
+ivPsbObqOgXUuBEU0xr9xMNBpU33ffLIsccrHq1EKp8zGfCOcww6v7+zEadUkVLo
+6j/rRhYYgRw9lijZG1rMuV/mTGnUqbjHsdoz5mzkFFWeTSqo44lvhveUyCcwRNmi
+2sjS77l0fCTzfreufffFoOEcRVMRfsnJdu/xPeARoXILEx8nQ421mSn6spOZlDQr
+Tt0T0BAWt+VNc+m0IGSW3SwS7r5MUyQ/M5GrbQBGi5W2SzPriKZ79YTOwPVmXKLZ
+vJoEuKRDkEPJLBAhcD5oSQljOm/Wp/hjmRH4HnI1y4XMshWlDsyRDB1Au5yrsfwN
+noFVSskEcbXlZfNgml4lktLBqz+qwsw+voq6Ak7ROKbc0ii5s8+iNMbAtIK7GcFF
+kuKKIyRmmGlDim/SDhlNdWo7Ah4Akde7zfWufwIDAQABo2AwXjAdBgNVHQ4EFgQU
+AY8+K4ZupAQ+L9ttFJG3vaLBq5gwDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwQFMAMB
+Af8wHwYDVR0jBBgwFoAUAY8+K4ZupAQ+L9ttFJG3vaLBq5gwDQYJKoZIhvcNAQEN
+BQADggIBAOpXi5o3g/2o2rPa53iG7Zgcy8RpePGgZk6xknGYWeLamEqSh+XWQZ2w
+2kQP54bf8HfPj3ugJBWsVtYAs/ltJwzeBfYDrwEJd1N8tw2IRuGlQOWiTAVVLBj4
+Zs+dikSuMoA399f/7BlUIEpVLUiV/emTtbkjFnDeKEV9zql6ypR0BtR8Knf8ALvL
+YfMsWLvTe4rXeypzxIaE2pn8ttcXLYAX0ml2MofTi5xcDhMn1vznKIvs82xhncQx
+I1MJMWqPHNHgJUJpA+y1IFh5LPbpag9PKQ0yQ9sM+/dyGumF2jElsMw71flh/Txr
+2dEv8+FNV1pPK26XJZBK24rNWFs30eAFfH9EQCwVla174I4PDoWqsIR7vtQMObDt
+Bq34R3TjjJJIt2sCSlYLooWwiK7Q+d/SgYqA+MSDmmwhzm86ToK6cwbCsvuw1AxR
+X6VIs4U8wOotgljzX/CSpKqlxcqZjhnAuelZ1+KiN8RHKPj7AzSLYOv/YwTjLTIq
+EOxquoNR58uDa5pBG22a7xWbSaKosn/mEl8SrUr6klzzc8Vh09IMoxrw74uLdAg2
+1jnrhm7qg91Ttb0aXiqbV+Kg/qQzojdewnnoBFnv4jaQ3y8zDCfMhsBtWlWz4Knb
+Zqga1WyRm3Gj1j6IV0oOincYMrw5YA7bgXpwop/Lo/mmliMA14ps
+-----END CERTIFICATE----- \ No newline at end of file
diff --git a/app/src/test/resources/preconfigured/demo.bitmask.net.json b/app/src/test/resources/preconfigured/demo.bitmask.net.json
new file mode 100644
index 00000000..e7fe6099
--- /dev/null
+++ b/app/src/test/resources/preconfigured/demo.bitmask.net.json
@@ -0,0 +1,42 @@
+{
+ "api_uri": "https://api.demo.bitmask.net:4430",
+ "api_version": "1",
+ "ca_cert_fingerprint": "SHA256: 0f17c033115f6b76ff67871872303ff65034efe7dd1b910062ca323eb4da5c7e",
+ "ca_cert_uri": "https://demo.bitmask.net/ca.crt",
+ "default_language": "en",
+ "description": {
+ "el": "demo.bitmask.net allows you to test the Bitmask application. User accounts may be periodically deleted.",
+ "en": "demo.bitmask.net allows you to test the Bitmask application. User accounts may be periodically deleted.",
+ "es": "demo.bitmask.net allows you to test the Bitmask application. User accounts may be periodically deleted."
+ },
+ "domain": "demo.bitmask.net",
+ "enrollment_policy": "open",
+ "languages": [
+ "de",
+ "en",
+ "es",
+ "pt"
+ ],
+ "name": {
+ "en": "Bitmask"
+ },
+ "service": {
+ "allow_anonymous": true,
+ "allow_free": true,
+ "allow_limited_bandwidth": false,
+ "allow_paid": false,
+ "allow_registration": true,
+ "allow_unlimited_bandwidth": true,
+ "bandwidth_limit": 102400,
+ "default_service_level": 1,
+ "levels": {
+ "1": {
+ "description": "Please donate.",
+ "name": "free"
+ }
+ }
+ },
+ "services": [
+ "openvpn"
+ ]
+} \ No newline at end of file
diff --git a/app/src/test/resources/preconfigured/demo.bitmask.net.pem b/app/src/test/resources/preconfigured/demo.bitmask.net.pem
new file mode 100644
index 00000000..9a422161
--- /dev/null
+++ b/app/src/test/resources/preconfigured/demo.bitmask.net.pem
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt
+YXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v
+Yml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw
+FgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV
+BAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
+ggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai
+dHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB
+7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r/bEHUSevmR09BRp86syHZerdNGpXYhcQ84
+CA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+
+znCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe/RGk4
+MEqGFuOzrtsgEhPIX0hplhb0Tgz/rtug+yTT7oJjBa3u20AAOQ38/M99EfdeJvc4
+lPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu/qt3DfvLfhboq+0
+bQvLUPXrVDr70onv5UDjpmEA/cLmaIqqrduuTkFZOym65/PfAPvpGnt7crQj/Ibl
+DEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB
+lfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy
+YMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw
+XjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH/BAQDAgIE
+MAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w
+DQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl
+cXYyB6hY5hv/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY
+k/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj
+RnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG
+htD/JKwt6xBmNwktH0GI/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX
+EIQ0ZR56bFuJA/CwValBqV/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J
+aF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l
+mlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK
+G6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co
+Ja8zlx64jmMZPg/t3wWqkZgXZ14qnbyG5/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d
+69db12/g4f6phldhxiWuGC/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e
+yV8e
+-----END CERTIFICATE----- \ No newline at end of file
diff --git a/app/src/test/resources/preconfigured/riseup.net.json b/app/src/test/resources/preconfigured/riseup.net.json
new file mode 100644
index 00000000..9a5ec79e
--- /dev/null
+++ b/app/src/test/resources/preconfigured/riseup.net.json
@@ -0,0 +1,37 @@
+{
+ "api_uri": "https://api.black.riseup.net:443",
+ "api_version": "1",
+ "ca_cert_fingerprint": "SHA256: a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494",
+ "ca_cert_uri": "https://black.riseup.net/ca.crt",
+ "default_language": "en",
+ "description": {
+ "en": "Riseup is a non-profit collective in Seattle that provides online communication tools for people and groups working toward liberatory social change."
+ },
+ "domain": "riseup.net",
+ "enrollment_policy": "open",
+ "languages": [
+ "en"
+ ],
+ "name": {
+ "en": "Riseup Networks"
+ },
+ "service": {
+ "allow_anonymous": false,
+ "allow_free": true,
+ "allow_limited_bandwidth": false,
+ "allow_paid": false,
+ "allow_registration": true,
+ "allow_unlimited_bandwidth": true,
+ "bandwidth_limit": 102400,
+ "default_service_level": 1,
+ "levels": {
+ "1": {
+ "description": "Please donate.",
+ "name": "free"
+ }
+ }
+ },
+ "services": [
+ "openvpn"
+ ]
+} \ No newline at end of file
diff --git a/app/src/test/resources/preconfigured/riseup.net.pem b/app/src/test/resources/preconfigured/riseup.net.pem
new file mode 100644
index 00000000..c890aff4
--- /dev/null
+++ b/app/src/test/resources/preconfigured/riseup.net.pem
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFjTCCA3WgAwIBAgIBATANBgkqhkiG9w0BAQ0FADBZMRgwFgYDVQQKDA9SaXNl
+dXAgTmV0d29ya3MxGzAZBgNVBAsMEmh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UE
+AwwXUmlzZXVwIE5ldHdvcmtzIFJvb3QgQ0EwHhcNMTQwNDI4MDAwMDAwWhcNMjQw
+NDI4MDAwMDAwWjBZMRgwFgYDVQQKDA9SaXNldXAgTmV0d29ya3MxGzAZBgNVBAsM
+Emh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UEAwwXUmlzZXVwIE5ldHdvcmtzIFJv
+b3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC76J4ciMJ8Sg0m
+TP7DF2DT9zNe0Csk4myoMFC57rfJeqsAlJCv1XMzBmXrw8wq/9z7XHv6n/0sWU7a
+7cF2hLR33ktjwODlx7vorU39/lXLndo492ZBhXQtG1INMShyv+nlmzO6GT7ESfNE
+LliFitEzwIegpMqxCIHXFuobGSCWF4N0qLHkq/SYUMoOJ96O3hmPSl1kFDRMtWXY
+iw1SEKjUvpyDJpVs3NGxeLCaA7bAWhDY5s5Yb2fA1o8ICAqhowurowJpW7n5ZuLK
+5VNTlNy6nZpkjt1QycYvNycffyPOFm/Q/RKDlvnorJIrihPkyniV3YY5cGgP+Qkx
+HUOT0uLA6LHtzfiyaOqkXwc4b0ZcQD5Vbf6Prd20Ppt6ei0zazkUPwxld3hgyw58
+m/4UIjG3PInWTNf293GngK2Bnz8Qx9e/6TueMSAn/3JBLem56E0WtmbLVjvko+LF
+PM5xA+m0BmuSJtrD1MUCXMhqYTtiOvgLBlUm5zkNxALzG+cXB28k6XikXt6MRG7q
+hzIPG38zwkooM55yy5i1YfcIi5NjMH6A+t4IJxxwb67MSb6UFOwg5kFokdONZcwj
+shczHdG9gLKSBIvrKa03Nd3W2dF9hMbRu//STcQxOailDBQCnXXfAATj9pYzdY4k
+ha8VCAREGAKTDAex9oXf1yRuktES4QIDAQABo2AwXjAdBgNVHQ4EFgQUC4tdmLVu
+f9hwfK4AGliaet5KkcgwDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwQFMAMBAf8wHwYD
+VR0jBBgwFoAUC4tdmLVuf9hwfK4AGliaet5KkcgwDQYJKoZIhvcNAQENBQADggIB
+AGzL+GRnYu99zFoy0bXJKOGCF5XUXP/3gIXPRDqQf5g7Cu/jYMID9dB3No4Zmf7v
+qHjiSXiS8jx1j/6/Luk6PpFbT7QYm4QLs1f4BlfZOti2KE8r7KRDPIecUsUXW6P/
+3GJAVYH/+7OjA39za9AieM7+H5BELGccGrM5wfl7JeEz8in+V2ZWDzHQO4hMkiTQ
+4ZckuaL201F68YpiItBNnJ9N5nHr1MRiGyApHmLXY/wvlrOpclh95qn+lG6/2jk7
+3AmihLOKYMlPwPakJg4PYczm3icFLgTpjV5sq2md9bRyAg3oPGfAuWHmKj2Ikqch
+Td5CHKGxEEWbGUWEMP0s1A/JHWiCbDigc4Cfxhy56CWG4q0tYtnc2GMw8OAUO6Wf
+Xu5pYKNkzKSEtT/MrNJt44tTZWbKV/Pi/N2Fx36my7TgTUj7g3xcE9eF4JV2H/sg
+tsK3pwE0FEqGnT4qMFbixQmc8bGyuakr23wjMvfO7eZUxBuWYR2SkcP26sozF9PF
+tGhbZHQVGZUTVPyvwahMUEhbPGVerOW0IYpxkm0x/eaWdTc4vPpf/rIlgbAjarnJ
+UN9SaWRlWKSdP4haujnzCoJbM7dU9bjvlGZNyXEekgeT0W2qFeGGp+yyUWw8tNsp
+0BuC1b7uW/bBn/xKm319wXVDvBgZgcktMolak39V7DVO
+-----END CERTIFICATE----- \ No newline at end of file
diff --git a/app/src/test/resources/preconfigured/urls/calyx.net.url b/app/src/test/resources/preconfigured/urls/calyx.net.url
new file mode 100644
index 00000000..807e9e18
--- /dev/null
+++ b/app/src/test/resources/preconfigured/urls/calyx.net.url
@@ -0,0 +1,3 @@
+{
+ "main_url" : "https://calyx.net"
+}
diff --git a/app/src/test/resources/preconfigured/urls/demo.bitmask.net.url b/app/src/test/resources/preconfigured/urls/demo.bitmask.net.url
new file mode 100644
index 00000000..0c4de648
--- /dev/null
+++ b/app/src/test/resources/preconfigured/urls/demo.bitmask.net.url
@@ -0,0 +1,3 @@
+{
+ "main_url" : "https://demo.bitmask.net"
+}
diff --git a/app/src/test/resources/preconfigured/urls/riseup.net.url b/app/src/test/resources/preconfigured/urls/riseup.net.url
new file mode 100644
index 00000000..42cdb979
--- /dev/null
+++ b/app/src/test/resources/preconfigured/urls/riseup.net.url
@@ -0,0 +1,3 @@
+{
+ "main_url" : "https://riseup.net"
+}
diff --git a/ics-openvpn b/ics-openvpn
-Subproject 984e58fea146fff53d2348d869ca4e1076cb9c9
+Subproject a727180b24969f7320c562925dabf27afd57c40