summaryrefslogtreecommitdiff
path: root/app/src
diff options
context:
space:
mode:
Diffstat (limited to 'app/src')
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java40
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/Provider.java3
-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/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
17 files changed, 328 insertions, 42 deletions
diff --git a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
index a84432e9..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;
@@ -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/Provider.java b/app/src/main/java/se/leap/bitmaskclient/Provider.java
index 98662783..fd067bf9 100644
--- a/app/src/main/java/se/leap/bitmaskclient/Provider.java
+++ b/app/src/main/java/se/leap/bitmaskclient/Provider.java
@@ -278,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();
}
@@ -428,5 +426,4 @@ public final class Provider implements Parcelable {
allowRegistered = false;
allowAnonymous = false;
}
-
}
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/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"
+}