summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorcyBerta <cyberta@riseup.net>2019-10-01 00:25:54 +0200
committercyBerta <cyberta@riseup.net>2019-10-01 00:25:54 +0200
commitfca6c9dcff1b5b5400a61b7411a7f72460fbfbfa (patch)
treec8efd8b964d495467619000fa7435c2a5527efa1 /app
parent7820e8c0819a10c5b4729678607681fcfe30cbae (diff)
parent685da193ea29f3e7a8a42d55747dfb2f956f23b6 (diff)
Merge branch 'pluggableTransports2'
Diffstat (limited to 'app')
-rw-r--r--app/build.gradle26
-rw-r--r--app/src/custom/assets/README_OBFS_OPENVPN_CONF.txt8
-rw-r--r--app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java21
-rw-r--r--app/src/main/AndroidManifest.xml4
-rw-r--r--app/src/main/java/de/blinkt/openvpn/VpnProfile.java46
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java93
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/ConnectionInterface.java15
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/NativeUtils.java13
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java36
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java16
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/ProfileManager.java0
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java219
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java59
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/connection/OpenvpnConnection.java13
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/Constants.java19
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/DrawerSettingsAdapter.java246
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java1
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/Provider.java25
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java10
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/StartActivity.java5
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/VpnNotificationManager.java56
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java320
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/EIP.java29
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/EipStatus.java11
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java121
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java2
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java40
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java203
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/BinaryInstaller.java204
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Dispatcher.java216
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Obfs4Options.java18
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Shapeshifter.java65
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java18
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/views/IconSwitchEntry.java104
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/views/IconTextEntry.java92
-rw-r--r--app/src/main/res/drawable-hdpi/ic_bridge_36.pngbin0 -> 821 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_bridge_36.pngbin0 -> 774 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_bridge_36.pngbin0 -> 1162 bytes
-rw-r--r--app/src/main/res/drawable-xxhdpi/ic_bridge_36.pngbin0 -> 1308 bytes
-rw-r--r--app/src/main/res/drawable-xxxhdpi/ic_bridge_36.pngbin0 -> 1760 bytes
-rw-r--r--app/src/main/res/layout-xlarge/v_icon_text_list_item.xml33
-rw-r--r--app/src/main/res/layout-xlarge/v_switch_list_item.xml25
-rw-r--r--app/src/main/res/layout/f_drawer_main.xml156
-rw-r--r--app/src/main/res/layout/v_icon_text_list_item.xml33
-rw-r--r--app/src/main/res/layout/v_switch_list_item.xml26
-rw-r--r--app/src/main/res/values/attrs.xml16
-rw-r--r--app/src/main/res/values/strings.xml5
-rw-r--r--app/src/main/res/values/themes.xml4
-rw-r--r--app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java37
-rw-r--r--app/src/sharedTest/java/se.leap.bitmaskclient/testutils/TestSetupHelper.java25
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/ProviderTest.java21
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/eip/EipStatusTest.java6
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java11
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java27
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java526
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BackendMockProvider.java3
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/EipSerivceJsonInvalidCertificateBackendResponse.java91
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java12
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java10
-rw-r--r--app/src/test/resources/openvpnConfigs/tcp_udp.ovpn117
-rw-r--r--app/src/test/resources/openvpnConfigs/udp_tcp.ovpn117
-rw-r--r--app/src/test/resources/ptdemo.bitmask.eip-service.json64
-rw-r--r--app/src/test/resources/ptdemo_pt_tcp_udp.eip-service.json65
-rw-r--r--app/src/test/resources/ptdemo_pt_udp_tcp.eip-service.json65
-rw-r--r--app/src/test/resources/ptdemo_three_mixed_gateways.json133
65 files changed, 3234 insertions, 768 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 7f391cb9..91cbeed1 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -21,6 +21,9 @@ android {
vectorDrawables.useSupportLibrary = true
buildConfigField 'boolean', 'openvpn3', 'false'
+ // USE PROTOTYPE DEMO SERVER CONFIG
+ buildConfigField 'boolean', 'useDemoConfig', 'false'
+
//Build Config Fields for default donation details
@@ -81,11 +84,11 @@ android {
//Configurations for custom branded app.
//Change the package name as needed, e.g. "org.example.myapp"
- applicationId "se.leap.riseupvpn"
+ applicationId "se.leap.pluggableTransportsDemo"
//Set app name here
- appName = "Riseup VPN"
+ appName = "Bitmask Pluggable Transports"
//Provider base url, e.g. '"https://example.com"'
- def customProviderUrl = '"https://riseup.net"'
+ def customProviderUrl = '"https://pt.demo.bitmask.net"'
buildConfigField "String", "customProviderUrl", customProviderUrl
//Change the versionCode as needed
//versionCode 1
@@ -98,14 +101,17 @@ android {
//Build Config Fields for default donation details
//This is the donation URL and should be set to the relevant donation page.
- buildConfigField 'String', 'donation_url', '"https://riseup.net/vpn/donate"'
+ buildConfigField 'String', 'donation_url', '""'
//The field to enable donations in the app.
- buildConfigField 'boolean', 'enable_donation', 'true'
+ buildConfigField 'boolean', 'enable_donation', 'false'
//The field to enable donation reminder popup in the app if enable_donation is set to 'false' this will be disabled.
buildConfigField 'boolean', 'enable_donation_reminder', 'true'
//The duration in days to trigger the donation reminder
buildConfigField 'int', 'donation_reminder_duration', '30'
+ // USE PROTOTYPE DEMO SERVER CONFIG
+ buildConfigField 'boolean', 'useDemoConfig', 'false'
+
//**************************************************************************
//**************************************************************************
@@ -137,12 +143,17 @@ android {
sourceSets {
main {
- assets.srcDirs = ['assets', 'ovpnlibs/assets', '../ics-openvpn/main/build/ovpnassets']
+ assets.srcDirs = ['assets',
+ 'ovpnlibs/assets',
+ '../ics-openvpn/main/build/ovpnassets',
+ // '../go/out' TODO: uncomment this line as soon as we want to use PT in production
+ ]
jniLibs.srcDirs = ['../ics-openvpn/main/build/intermediates/cmake/noovpn3/release/obj']
jni.srcDirs = [] //disable automatic ndk-build
}
debug {
- assets.srcDirs = ['src/debug/assets']
+ assets.srcDirs = ['src/debug/assets',
+ /*'../go/out'*/]
}
test {
@@ -196,6 +207,7 @@ dependencies {
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.android.support:multidex:1.0.3'
implementation 'com.android.support:cardview-v7:28.0.0'
+ implementation project(path: ':shapeshifter')
}
android.applicationVariants.all { variant ->
diff --git a/app/src/custom/assets/README_OBFS_OPENVPN_CONF.txt b/app/src/custom/assets/README_OBFS_OPENVPN_CONF.txt
new file mode 100644
index 00000000..ceebe1f1
--- /dev/null
+++ b/app/src/custom/assets/README_OBFS_OPENVPN_CONF.txt
@@ -0,0 +1,8 @@
+obfs proxy is currently running on
+"37.218.247.60"
+
+the openvpn is running on
+"37.218.242.216"
+
+When testing either an obfuscated or a plain openvpn connection you
+need to switch the ips in "ptdemo.bitmask.eip-service.json" \ No newline at end of file
diff --git a/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java b/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java
index 1190d382..e79946bf 100644
--- a/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java
+++ b/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java
@@ -135,8 +135,8 @@ public class ProviderApiManager extends ProviderApiManagerBase {
private Bundle getAndSetProviderJson(Provider provider, boolean dangerOn) {
Bundle result = new Bundle();
- JSONObject providerDefinition = provider.getDefinition();
String caCert = provider.getCaCert();
+ JSONObject providerDefinition = provider.getDefinition();
String providerMainUrl = provider.getMainUrlString();
String providerDotJsonString;
@@ -161,10 +161,9 @@ public class ProviderApiManager extends ProviderApiManagerBase {
result.putBoolean(BROADCAST_RESULT_KEY, true);
} catch (JSONException e) {
- String reason_to_fail = pickErrorMessage(providerDotJsonString);
- result.putString(ERRORS, reason_to_fail);
- result.putBoolean(BROADCAST_RESULT_KEY, false);
+ setErrorResult(result, providerDotJsonString);
}
+ //TODO: check why the following line is not in production
result.putParcelable(PROVIDER_KEY, provider);
return result;
}
@@ -185,18 +184,15 @@ public class ProviderApiManager extends ProviderApiManagerBase {
JSONObject eipServiceJson = new JSONObject(eipServiceJsonString);
if (eipServiceJson.has(ERRORS)) {
- String reasonToFail = pickErrorMessage(eipServiceJsonString);
- result.putString(ERRORS, reasonToFail);
- result.putBoolean(BROADCAST_RESULT_KEY, false);
+ setErrorResult(result, eipServiceJsonString);
} else{
provider.setEipServiceJson(eipServiceJson);
result.putBoolean(BROADCAST_RESULT_KEY, true);
}
} catch (NullPointerException | JSONException e) {
- String reasonToFail = pickErrorMessage(eipServiceJsonString);
- result.putString(ERRORS, reasonToFail);
- result.putBoolean(BROADCAST_RESULT_KEY, false);
+ setErrorResult(result, eipServiceJsonString);
}
+ //TODO: check why the following line is not in production
result.putParcelable(PROVIDER_KEY, provider);
return result;
}
@@ -218,9 +214,7 @@ public class ProviderApiManager extends ProviderApiManagerBase {
// probably 204
setErrorResult(result, error_io_exception_user_message, null);
} else {
- String reasonToFail = pickErrorMessage(certString);
- result.putString(ERRORS, reasonToFail);
- result.putBoolean(BROADCAST_RESULT_KEY, false);
+ setErrorResult(result, certString);
return result;
}
}
@@ -229,6 +223,7 @@ public class ProviderApiManager extends ProviderApiManagerBase {
setErrorResult(result, downloading_vpn_certificate_failed, null);
e.printStackTrace();
}
+ //TODO: check why the following line is not in production
result.putParcelable(PROVIDER_KEY, provider);
return result;
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a00582cc..9a2b1e43 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -62,10 +62,10 @@
<activity
android:name=".eip.VoidVpnLauncher"
- android:theme="@android:style/Theme.Translucent.NoTitleBar" />
+ android:theme="@style/invisibleTheme" />
<activity
android:name="de.blinkt.openvpn.LaunchVPN"
- android:label="@string/vpn_launch_title" />
+ android:theme="@style/invisibleTheme" />
<activity
android:name=".StartActivity"
android:label="@string/app_name"
diff --git a/app/src/main/java/de/blinkt/openvpn/VpnProfile.java b/app/src/main/java/de/blinkt/openvpn/VpnProfile.java
index 7b9003aa..f139fdc9 100644
--- a/app/src/main/java/de/blinkt/openvpn/VpnProfile.java
+++ b/app/src/main/java/de/blinkt/openvpn/VpnProfile.java
@@ -53,7 +53,6 @@ import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
-import de.blinkt.openvpn.core.Connection;
import de.blinkt.openvpn.core.ExtAuthHelper;
import de.blinkt.openvpn.core.NativeUtils;
import de.blinkt.openvpn.core.OpenVPNService;
@@ -63,9 +62,13 @@ import de.blinkt.openvpn.core.Preferences;
import de.blinkt.openvpn.core.VPNLaunchHelper;
import de.blinkt.openvpn.core.VpnStatus;
import de.blinkt.openvpn.core.X509Utils;
+import de.blinkt.openvpn.core.connection.Connection;
+import de.blinkt.openvpn.core.connection.Obfs4Connection;
+import de.blinkt.openvpn.core.connection.OpenvpnConnection;
import se.leap.bitmaskclient.BuildConfig;
import se.leap.bitmaskclient.R;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
import static se.leap.bitmaskclient.Constants.PROVIDER_PROFILE;
public class VpnProfile implements Serializable, Cloneable {
@@ -116,7 +119,7 @@ public class VpnProfile implements Serializable, Cloneable {
public String mTLSAuthFilename;
public String mClientKeyFilename;
public String mCaFilename;
- public boolean mUseLzo = true;
+ public boolean mUseLzo = false;
public String mPKCS12Filename;
public String mPKCS12Password;
public boolean mUseTLSAuth = false;
@@ -171,6 +174,7 @@ public class VpnProfile implements Serializable, Cloneable {
// timestamp when the profile was last used
public long mLastUsed;
public String importedProfileHash;
+ //TODO: cleanup here
/* Options no longer used in new profiles */
public String mServerName = "openvpn.example.com";
public String mServerPort = "1194";
@@ -181,16 +185,17 @@ public class VpnProfile implements Serializable, Cloneable {
// set members to default values
private UUID mUuid;
private int mProfileVersion;
+ public String mGatewayIp;
+ public boolean mUsePluggableTransports;
-
- public VpnProfile(String name) {
+ public VpnProfile(String name, Connection.TransportType transportType) {
mUuid = UUID.randomUUID();
mName = name;
mProfileVersion = CURRENT_PROFILE_VERSION;
mConnections = new Connection[1];
- mConnections[0] = new Connection();
mLastUsed = System.currentTimeMillis();
+ mUsePluggableTransports = transportType == OBFS4;
}
public static String openVpnEscape(String unescaped) {
@@ -292,6 +297,7 @@ public class VpnProfile implements Serializable, Cloneable {
return mName;
}
+ @Deprecated
public void upgradeProfile() {
if (mProfileVersion < 2) {
/* default to the behaviour the OS used */
@@ -314,22 +320,23 @@ public class VpnProfile implements Serializable, Cloneable {
}
if (mProfileVersion < 7) {
for (Connection c : mConnections)
- if (c.mProxyType == null)
- c.mProxyType = Connection.ProxyType.NONE;
+ if (c.getProxyType() == null)
+ c.setProxyType(Connection.ProxyType.NONE);
}
mProfileVersion = CURRENT_PROFILE_VERSION;
}
+ @Deprecated
private void moveOptionsToConnection() {
mConnections = new Connection[1];
- Connection conn = new Connection();
+ Connection conn = mUsePluggableTransports ? new Obfs4Connection() : new OpenvpnConnection();
- conn.mServerName = mServerName;
- conn.mServerPort = mServerPort;
- conn.mUseUdp = mUseUdp;
- conn.mCustomConfiguration = "";
+ conn.setServerName(mServerName);
+ conn.setServerPort(mServerPort);
+ conn.setUseUdp(mUseUdp);
+ conn.setCustomConfiguration("");
mConnections[0] = conn;
@@ -425,7 +432,7 @@ public class VpnProfile implements Serializable, Cloneable {
if (canUsePlainRemotes) {
for (Connection conn : mConnections) {
- if (conn.mEnabled) {
+ if (conn.isEnabled()) {
cfg.append(conn.getConnectionBlock(configForOvpn3));
}
}
@@ -494,7 +501,8 @@ public class VpnProfile implements Serializable, Cloneable {
if (!TextUtils.isEmpty(mCrlFilename))
cfg.append(insertFileData("crl-verify", mCrlFilename));
- if (mUseLzo) {
+ // compression does not work in conjunction with shapeshifter-dispatcher so far
+ if (mUseLzo && !mUsePluggableTransports) {
cfg.append("comp-lzo\n");
}
@@ -586,7 +594,7 @@ public class VpnProfile implements Serializable, Cloneable {
if (mAuthenticationType != TYPE_STATICKEYS) {
if (mCheckRemoteCN) {
if (mRemoteCN == null || mRemoteCN.equals(""))
- cfg.append("verify-x509-name ").append(openVpnEscape(mConnections[0].mServerName)).append(" name\n");
+ cfg.append("verify-x509-name ").append(openVpnEscape(mConnections[0].getServerName())).append(" name\n");
else
switch (mX509AuthType) {
@@ -660,7 +668,7 @@ public class VpnProfile implements Serializable, Cloneable {
if (!canUsePlainRemotes) {
cfg.append("# Connection Options are at the end to allow global options (and global custom options) to influence connection blocks\n");
for (Connection conn : mConnections) {
- if (conn.mEnabled) {
+ if (conn.isEnabled()) {
cfg.append("<connection>\n");
cfg.append(conn.getConnectionBlock(configForOvpn3));
cfg.append("</connection>\n");
@@ -985,7 +993,7 @@ public class VpnProfile implements Serializable, Cloneable {
boolean noRemoteEnabled = true;
for (Connection c : mConnections) {
- if (c.mEnabled)
+ if (c.isEnabled())
noRemoteEnabled = false;
}
@@ -1000,12 +1008,12 @@ public class VpnProfile implements Serializable, Cloneable {
return R.string.openvpn3_pkcs12;
}
for (Connection conn : mConnections) {
- if (conn.mProxyType == Connection.ProxyType.ORBOT || conn.mProxyType == Connection.ProxyType.SOCKS5)
+ if (conn.getProxyType() == Connection.ProxyType.ORBOT || conn.getProxyType() == Connection.ProxyType.SOCKS5)
return R.string.openvpn3_socksproxy;
}
}
for (Connection c : mConnections) {
- if (c.mProxyType == Connection.ProxyType.ORBOT) {
+ if (c.getProxyType() == Connection.ProxyType.ORBOT) {
if (usesExtraProxyOptions())
return R.string.error_orbot_and_proxy_options;
if (!OrbotHelper.checkTorReceier(context))
diff --git a/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java b/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java
index 0148bfb7..5ccd83dd 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java
@@ -13,9 +13,21 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
-import java.util.*;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Vector;
import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.connection.Connection;
+import de.blinkt.openvpn.core.connection.Obfs4Connection;
+import de.blinkt.openvpn.core.connection.OpenvpnConnection;
+import se.leap.bitmaskclient.pluggableTransports.Obfs4Options;
+
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
//! Openvpn Config FIle Parser, probably not 100% accurate but close enough
@@ -128,6 +140,7 @@ public class ConfigParser {
private HashMap<String, Vector<Vector<String>>> options = new HashMap<>();
private HashMap<String, Vector<String>> meta = new HashMap<String, Vector<String>>();
private String auth_user_pass_file;
+ private Obfs4Options obfs4Options;
static public void useEmbbedUserAuth(VpnProfile np, String inlinedata) {
String data = VpnProfile.getEmbeddedContent(inlinedata);
@@ -142,9 +155,9 @@ public class ConfigParser {
String data = VpnProfile.getEmbeddedContent(inlinedata);
String[] parts = data.split("\n");
if (parts.length >= 2) {
- c.mProxyAuthUser = parts[0];
- c.mProxyAuthPassword = parts[1];
- c.mUseProxyAuth = true;
+ c.setProxyAuthUser(parts[0]);
+ c.setProxyAuthPassword(parts[1]);
+ c.setUseProxyAuth(true);
}
}
@@ -338,9 +351,9 @@ public class ConfigParser {
// This method is far too long
@SuppressWarnings("ConstantConditions")
- public VpnProfile convertProfile() throws ConfigParseError, IOException {
+ public VpnProfile convertProfile(Connection.TransportType transportType) throws ConfigParseError, IOException {
boolean noauthtypeset = true;
- VpnProfile np = new VpnProfile(CONVERTED_PROFILE);
+ VpnProfile np = new VpnProfile(CONVERTED_PROFILE, transportType);
// Pull, client, tls-client
np.clearDefaults();
@@ -443,6 +456,7 @@ public class ConfigParser {
if (redirectPrivate != null) {
checkRedirectParameters(np, redirectPrivate, false);
}
+
Vector<String> dev = getOption("dev", 1, 1);
Vector<String> devtype = getOption("dev-type", 1, 1);
@@ -468,7 +482,6 @@ public class ConfigParser {
}
}
-
Vector<String> tunmtu = getOption("tun-mtu", 1, 1);
if (tunmtu != null) {
@@ -479,14 +492,12 @@ public class ConfigParser {
}
}
-
Vector<String> mode = getOption("mode", 1, 1);
if (mode != null) {
if (!mode.get(1).equals("p2p"))
throw new ConfigParseError("Invalid mode for --mode specified, need p2p");
}
-
Vector<Vector<String>> dhcpoptions = getAllOption("dhcp-option", 2, 2);
if (dhcpoptions != null) {
for (Vector<String> dhcpoption : dhcpoptions) {
@@ -521,8 +532,10 @@ public class ConfigParser {
if (getOption("float", 0, 0) != null)
np.mUseFloat = true;
- if (getOption("comp-lzo", 0, 1) != null)
- np.mUseLzo = true;
+ Vector<String> useLzo = getOption("comp-lzo", 0, 1);
+ if (useLzo != null) {
+ np.mUseLzo = Boolean.valueOf(useLzo.get(1));
+ }
Vector<String> cipher = getOption("cipher", 1, 1);
if (cipher != null)
@@ -532,7 +545,6 @@ public class ConfigParser {
if (auth != null)
np.mAuth = auth.get(1);
-
Vector<String> ca = getOption("ca", 1, 1);
if (ca != null) {
np.mCaFilename = ca.get(1);
@@ -544,6 +556,7 @@ public class ConfigParser {
np.mAuthenticationType = VpnProfile.TYPE_CERTIFICATES;
noauthtypeset = false;
}
+
Vector<String> key = getOption("key", 1, 1);
if (key != null)
np.mClientKeyFilename = key.get(1);
@@ -604,8 +617,7 @@ public class ConfigParser {
np.mVerb = verb.get(1);
}
-
- if (getOption("nobind", 0, 0) != null)
+ if (getOption("nobind", 0, 1) != null)
np.mNobind = true;
if (getOption("persist-tun", 0, 0) != null)
@@ -674,8 +686,7 @@ public class ConfigParser {
}
-
- Pair<Connection, Connection[]> conns = parseConnectionOptions(null);
+ Pair<Connection, Connection[]> conns = parseConnectionOptions(null, transportType);
np.mConnections = conns.second;
Vector<Vector<String>> connectionBlocks = getAllOption("connection", 1, 1);
@@ -698,6 +709,7 @@ public class ConfigParser {
connIndex++;
}
}
+
if (getOption("remote-random", 0, 0) != null)
np.mRemoteRandom = true;
@@ -713,8 +725,8 @@ public class ConfigParser {
throw new ConfigParseError(String.format("Unknown protocol %s in proto-force", protoToDisable));
for (Connection conn : np.mConnections)
- if (conn.mUseUdp == disableUDP)
- conn.mEnabled = false;
+ if (conn.isUseUdp() == disableUDP)
+ conn.setEnabled(false);
}
// Parse OpenVPN Access Server extra
@@ -740,20 +752,21 @@ public class ConfigParser {
return TextUtils.join(s, str);
}
+ public void setObfs4Options(Obfs4Options obfs4Options) {
+ this.obfs4Options = obfs4Options;
+ }
+
private Pair<Connection, Connection[]> parseConnection(String connection, Connection defaultValues) throws IOException, ConfigParseError {
// Parse a connection Block as a new configuration file
-
ConfigParser connectionParser = new ConfigParser();
StringReader reader = new StringReader(connection.substring(VpnProfile.INLINE_TAG.length()));
connectionParser.parseConfig(reader);
- Pair<Connection, Connection[]> conn = connectionParser.parseConnectionOptions(defaultValues);
-
- return conn;
+ return connectionParser.parseConnectionOptions(defaultValues, defaultValues.getTransportType());
}
- private Pair<Connection, Connection[]> parseConnectionOptions(Connection connDefault) throws ConfigParseError {
+ private Pair<Connection, Connection[]> parseConnectionOptions(Connection connDefault, Connection.TransportType transportType) throws ConfigParseError {
Connection conn;
if (connDefault != null)
try {
@@ -763,27 +776,27 @@ public class ConfigParser {
return null;
}
else
- conn = new Connection();
+ conn = transportType == OBFS4 ? new Obfs4Connection(obfs4Options) : new OpenvpnConnection();
Vector<String> port = getOption("port", 1, 1);
if (port != null) {
- conn.mServerPort = port.get(1);
+ conn.setServerPort(port.get(1));
}
Vector<String> rport = getOption("rport", 1, 1);
if (rport != null) {
- conn.mServerPort = rport.get(1);
+ conn.setServerPort(rport.get(1));
}
Vector<String> proto = getOption("proto", 1, 1);
if (proto != null) {
- conn.mUseUdp = isUdpProto(proto.get(1));
+ conn.setUseUdp(isUdpProto(proto.get(1)));
}
Vector<String> connectTimeout = getOption("connect-timeout", 1, 1);
if (connectTimeout != null) {
try {
- conn.mConnectTimeout = Integer.parseInt(connectTimeout.get(1));
+ conn.setConnectTimeout(Integer.parseInt(connectTimeout.get(1)));
} catch (NumberFormatException nfe) {
throw new ConfigParseError(String.format("Argument to connect-timeout (%s) must to be an integer: %s",
connectTimeout.get(1), nfe.getLocalizedMessage()));
@@ -797,16 +810,16 @@ public class ConfigParser {
if (proxy != null) {
if (proxy.get(0).equals("socks-proxy")) {
- conn.mProxyType = Connection.ProxyType.SOCKS5;
+ conn.setProxyType(Connection.ProxyType.SOCKS5);
// socks defaults to 1080, http always sets port
- conn.mProxyPort = "1080";
+ conn.setProxyPort("1080");
} else {
- conn.mProxyType = Connection.ProxyType.HTTP;
+ conn.setProxyType(Connection.ProxyType.HTTP);
}
- conn.mProxyName = proxy.get(1);
+ conn.setProxyName(proxy.get(1));
if (proxy.size() >= 3)
- conn.mProxyPort = proxy.get(2);
+ conn.setProxyPort(proxy.get(2));
}
Vector<String> httpproxyauthhttp = getOption("http-proxy-user-pass", 1, 1);
@@ -817,21 +830,19 @@ public class ConfigParser {
// Parse remote config
Vector<Vector<String>> remotes = getAllOption("remote", 1, 3);
-
-
Vector <String> optionsToRemove = new Vector<>();
// Assume that we need custom options if connectionDefault are set or in the connection specific set
for (Map.Entry<String, Vector<Vector<String>>> option : options.entrySet()) {
if (connDefault != null || connectionOptionsSet.contains(option.getKey())) {
- conn.mCustomConfiguration += getOptionStrings(option.getValue());
+ conn.setCustomConfiguration(conn.getCustomConfiguration() + getOptionStrings(option.getValue()));
optionsToRemove.add(option.getKey());
}
}
for (String o: optionsToRemove)
options.remove(o);
- if (!(conn.mCustomConfiguration == null || "".equals(conn.mCustomConfiguration.trim())))
- conn.mUseCustomConfig = true;
+ if (!(conn.getCustomConfiguration() == null || "".equals(conn.getCustomConfiguration().trim())))
+ conn.setUseCustomConfig(true);
// Make remotes empty to simplify code
if (remotes == null)
@@ -849,11 +860,11 @@ public class ConfigParser {
}
switch (remote.size()) {
case 4:
- connections[i].mUseUdp = isUdpProto(remote.get(3));
+ connections[i].setUseUdp(isUdpProto(remote.get(3)));
case 3:
- connections[i].mServerPort = remote.get(2);
+ connections[i].setServerPort(remote.get(2));
case 2:
- connections[i].mServerName = remote.get(1);
+ connections[i].setServerName(remote.get(1));
}
i++;
}
diff --git a/app/src/main/java/de/blinkt/openvpn/core/ConnectionInterface.java b/app/src/main/java/de/blinkt/openvpn/core/ConnectionInterface.java
new file mode 100644
index 00000000..70b4b4ec
--- /dev/null
+++ b/app/src/main/java/de/blinkt/openvpn/core/ConnectionInterface.java
@@ -0,0 +1,15 @@
+package de.blinkt.openvpn.core;
+
+import java.io.Serializable;
+
+/**
+ * Created by cyberta on 11.03.19.
+ */
+
+public interface ConnectionInterface {
+
+ String getConnectionBlock(boolean isOpenVPN3);
+ boolean usesExtraProxyOptions();
+ boolean isOnlyRemote();
+ int getTimeout();
+}
diff --git a/app/src/main/java/de/blinkt/openvpn/core/NativeUtils.java b/app/src/main/java/de/blinkt/openvpn/core/NativeUtils.java
index 6b633c34..a66b7b51 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/NativeUtils.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/NativeUtils.java
@@ -20,6 +20,8 @@ public class NativeUtils {
{
if (isRoboUnitTest())
return "ROBO";
+ else if (isUnitTest())
+ return "JUNIT";
else
return getJNIAPI();
}
@@ -34,7 +36,7 @@ public class NativeUtils {
public static native double[] getOpenSSLSpeed(String algorithm, int testnum);
static {
- if (!isRoboUnitTest()) {
+ if (!isRoboUnitTest() && !isUnitTest()) {
System.loadLibrary("opvpnutil");
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN)
System.loadLibrary("jbcrypto");
@@ -44,4 +46,13 @@ public class NativeUtils {
public static boolean isRoboUnitTest() {
return "robolectric".equals(Build.FINGERPRINT);
}
+
+ public static boolean isUnitTest() {
+ try {
+ Class.forName("se.leap.bitmaskclient.testutils.MockHelper");
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
}
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 82c4e1df..e446021f 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java
@@ -42,8 +42,11 @@ import java.util.Vector;
import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.VpnStatus.ByteCountListener;
import de.blinkt.openvpn.core.VpnStatus.StateListener;
+import de.blinkt.openvpn.core.connection.Connection;
+import de.blinkt.openvpn.core.connection.Obfs4Connection;
import se.leap.bitmaskclient.R;
import se.leap.bitmaskclient.VpnNotificationManager;
+import se.leap.bitmaskclient.pluggableTransports.Shapeshifter;
import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_CONNECTED;
import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT;
@@ -52,6 +55,7 @@ import static se.leap.bitmaskclient.Constants.PROVIDER_PROFILE;
public class OpenVPNService extends VpnService implements StateListener, Callback, ByteCountListener, IOpenVPNServiceInternal, VpnNotificationManager.VpnServiceCallback {
+ public static final String TAG = OpenVPNService.class.getSimpleName();
public static final String START_SERVICE = "de.blinkt.openvpn.START_SERVICE";
public static final String START_SERVICE_STICKY = "de.blinkt.openvpn.START_SERVICE_STICKY";
public static final String ALWAYS_SHOW_NOTIFICATION = "de.blinkt.openvpn.NOTIFICATION_ALWAYS_VISIBLE";
@@ -62,7 +66,6 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
public final static String ORBOT_PACKAGE_NAME = "org.torproject.android";
private static final String PAUSE_VPN = "de.blinkt.openvpn.PAUSE_VPN";
private static final String RESUME_VPN = "se.leap.bitmaskclient.RESUME_VPN";
- private static final String TAG = OpenVPNService.class.getSimpleName();
private static boolean mNotificationAlwaysVisible = false;
private final Vector<String> mDnslist = new Vector<>();
private final NetworkSpace mRoutes = new NetworkSpace();
@@ -85,6 +88,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
private Toast mlastToast;
private Runnable mOpenVPNThread;
private VpnNotificationManager notificationManager;
+ private Shapeshifter shapeshifter;
private static final int PRIORITY_MIN = -2;
private static final int PRIORITY_DEFAULT = 0;
@@ -242,6 +246,9 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
if(isVpnRunning()) {
if (getManagement() != null && getManagement().stopVPN(replaceConnection)) {
if (!replaceConnection) {
+ if (shapeshifter != null) {
+ shapeshifter.stop();
+ }
VpnStatus.updateStateString("NOPROCESS", "VPN STOPPED", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED);
}
return true;
@@ -249,6 +256,9 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
return false;
} else {
if (!replaceConnection) {
+ if (shapeshifter != null) {
+ shapeshifter.stop();
+ }
VpnStatus.updateStateString("NOPROCESS", "VPN STOPPED", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED);
return true;
}
@@ -307,6 +317,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
VpnStatus.updateStateString("VPN_GENERATE_CONFIG", "", R.string.building_configration, ConnectionStatus.LEVEL_START);
notificationManager.buildOpenVpnNotification(
mProfile != null ? mProfile.mName : "",
+ mProfile != null && mProfile.mUsePluggableTransports,
VpnStatus.getLastCleanLogMessage(this),
VpnStatus.getLastCleanLogMessage(this),
ConnectionStatus.LEVEL_START,
@@ -366,10 +377,26 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
/**
* see change above (l. 292 ff)
*/
+ //TODO: investigate how connections[n] with n>0 get called during vpn setup (on connection refused?)
+ // Do we need to check if there's any obfs4 connection in mProfile.mConnections and start
+ // the dispatcher here? Can we start the dispatcher at a later point of execution, e.g. when
+ // connections[n], n>0 gets choosen?
+
+ Connection connection = mProfile.mConnections[0];
+
+ if (mProfile.mUsePluggableTransports) {
+ Obfs4Connection obfs4Connection = (Obfs4Connection) connection;
+ shapeshifter = new Shapeshifter(obfs4Connection.getDispatcherOptions());
+ if (!shapeshifter.start()) {
+ //TODO: implement useful error handling
+ Log.e(TAG, "Cannot initialize shapeshifter dispatcher for obfs4 connection. Shutting down.");
+ VpnStatus.logError("Cannot initialize shapeshifter dispatcher for obfs4 connection. Shutting down.");
+ }
+ }
+
VpnStatus.logInfo(R.string.building_configration);
VpnStatus.updateStateString("VPN_GENERATE_CONFIG", "", R.string.building_configration, ConnectionStatus.LEVEL_START);
-
try {
mProfile.writeConfigFile(this);
} catch (IOException e) {
@@ -743,7 +770,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
boolean profileUsesOrBot = false;
for (Connection c : mProfile.mConnections) {
- if (c.mProxyType == Connection.ProxyType.ORBOT)
+ if (c.getProxyType() == Connection.ProxyType.ORBOT)
profileUsesOrBot = true;
}
@@ -951,6 +978,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
// Does not work :(
notificationManager.buildOpenVpnNotification(
mProfile != null ? mProfile.mName : "",
+ mProfile != null && mProfile.mUsePluggableTransports,
VpnStatus.getLastCleanLogMessage(this),
VpnStatus.getLastCleanLogMessage(this),
level,
@@ -982,6 +1010,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
humanReadableByteCount(diffOut / OpenVPNManagement.mBytecountInterval, true, getResources()));
notificationManager.buildOpenVpnNotification(
mProfile != null ? mProfile.mName : "",
+ mProfile != null && mProfile.mUsePluggableTransports,
netstat,
null,
LEVEL_CONNECTED,
@@ -1025,6 +1054,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
VpnStatus.updateStateString("NEED", "need " + needed, resid, LEVEL_WAITING_FOR_USER_INPUT);
notificationManager.buildOpenVpnNotification(
mProfile != null ? mProfile.mName : "",
+ mProfile != null && mProfile.mUsePluggableTransports,
getString(resid),
getString(resid),
LEVEL_WAITING_FOR_USER_INPUT,
diff --git a/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java b/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java
index 4f7a5bda..91cc66bc 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java
@@ -15,9 +15,10 @@ import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
-import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;
+
+import de.blinkt.openvpn.core.connection.Connection;
import se.leap.bitmaskclient.R;
import de.blinkt.openvpn.VpnProfile;
@@ -452,10 +453,10 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement {
if (mProfile.mConnections.length > connectionEntryNumber) {
Connection connection = mProfile.mConnections[connectionEntryNumber];
- proxyType = connection.mProxyType;
- proxyname = connection.mProxyName;
- proxyport = connection.mProxyPort;
- proxyUseAuth = connection.mUseProxyAuth;
+ proxyType = connection.getProxyType();
+ proxyname = connection.getProxyName();
+ proxyport = connection.getProxyPort();
+ proxyUseAuth = connection.isUseProxyAuth();
// Use transient variable to remember http user/password
mCurrentProxyConnection = connection;
@@ -696,8 +697,8 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement {
} else if (needed.equals("HTTP Proxy")) {
if( mCurrentProxyConnection != null) {
- pw = mCurrentProxyConnection.mProxyAuthPassword;
- username = mCurrentProxyConnection.mProxyAuthUser;
+ pw = mCurrentProxyConnection.getProxyAuthPassword();
+ username = mCurrentProxyConnection.getProxyAuthUser();
}
}
if (pw != null) {
@@ -782,7 +783,6 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement {
boolean stopSucceed = stopOpenVPN();
if (stopSucceed) {
mShuttingDown = true;
-
}
return stopSucceed;
}
diff --git a/app/src/main/java/de/blinkt/openvpn/core/ProfileManager.java b/app/src/main/java/de/blinkt/openvpn/core/ProfileManager.java
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/app/src/main/java/de/blinkt/openvpn/core/ProfileManager.java
diff --git a/app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java b/app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java
new file mode 100644
index 00000000..a318e55d
--- /dev/null
+++ b/app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (c) 2012-2016 Arne Schwabe
+ * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt
+ */
+
+package de.blinkt.openvpn.core.connection;
+
+import android.text.TextUtils;
+
+import java.io.Serializable;
+import java.util.Locale;
+
+public abstract class Connection implements Serializable, Cloneable {
+ private String mServerName = "openvpn.example.com";
+ private String mServerPort = "1194";
+ private boolean mUseUdp = true;
+ private String mCustomConfiguration = "";
+ private boolean mUseCustomConfig = false;
+ private boolean mEnabled = true;
+ private int mConnectTimeout = 0;
+ private static final int CONNECTION_DEFAULT_TIMEOUT = 120;
+ private ProxyType mProxyType = ProxyType.NONE;
+ private String mProxyName = "proxy.example.com";
+ private String mProxyPort = "8080";
+
+ private boolean mUseProxyAuth;
+ private String mProxyAuthUser = null;
+ private String mProxyAuthPassword = null;
+
+ public enum ProxyType {
+ NONE,
+ HTTP,
+ SOCKS5,
+ ORBOT
+ }
+
+ public enum TransportType {
+ OBFS4("obfs4"),
+ OPENVPN("openvpn");
+
+ String transport;
+
+ TransportType(String transportType) {
+ this.transport = transportType;
+ }
+
+ @Override
+ public String toString() {
+ return transport;
+ }
+ }
+
+
+ private static final long serialVersionUID = 92031902903829089L;
+
+
+ public String getConnectionBlock(boolean isOpenVPN3) {
+ String cfg = "";
+
+ // Server Address
+ cfg += "remote ";
+ cfg += mServerName;
+ cfg += " ";
+ cfg += mServerPort;
+ if (mUseUdp)
+ cfg += " udp\n";
+ else
+ cfg += " tcp-client\n";
+
+ if (mConnectTimeout != 0)
+ cfg += String.format(Locale.US, " connect-timeout %d\n", mConnectTimeout);
+
+ // OpenVPN 2.x manages proxy connection via management interface
+ if ((isOpenVPN3 || usesExtraProxyOptions()) && mProxyType == ProxyType.HTTP)
+ {
+ cfg+=String.format(Locale.US,"http-proxy %s %s\n", mProxyName, mProxyPort);
+ if (mUseProxyAuth)
+ cfg+=String.format(Locale.US, "<http-proxy-user-pass>\n%s\n%s\n</http-proxy-user-pass>\n", mProxyAuthUser, mProxyAuthPassword);
+ }
+ if (usesExtraProxyOptions() && mProxyType == ProxyType.SOCKS5) {
+ cfg+=String.format(Locale.US,"socks-proxy %s %s\n", mProxyName, mProxyPort);
+ }
+
+ if (!TextUtils.isEmpty(mCustomConfiguration) && mUseCustomConfig) {
+ cfg += mCustomConfiguration;
+ cfg += "\n";
+ }
+
+
+ return cfg;
+ }
+
+ public boolean usesExtraProxyOptions() {
+ return (mUseCustomConfig && mCustomConfiguration.contains("http-proxy-option "));
+ }
+
+
+ @Override
+ public Connection clone() throws CloneNotSupportedException {
+ return (Connection) super.clone();
+ }
+
+ public boolean isOnlyRemote() {
+ return TextUtils.isEmpty(mCustomConfiguration) || !mUseCustomConfig;
+ }
+
+ public int getTimeout() {
+ if (mConnectTimeout <= 0)
+ return CONNECTION_DEFAULT_TIMEOUT;
+ else
+ return mConnectTimeout;
+ }
+
+ public String getServerName() {
+ return mServerName;
+ }
+
+ public void setServerName(String mServerName) {
+ this.mServerName = mServerName;
+ }
+
+ public String getServerPort() {
+ return mServerPort;
+ }
+
+ public void setServerPort(String serverPort) {
+ this.mServerPort = serverPort;
+ }
+
+ public boolean isUseUdp() {
+ return mUseUdp;
+ }
+
+ public void setUseUdp(boolean useUdp) {
+ this.mUseUdp = useUdp;
+ }
+
+ public String getCustomConfiguration() {
+ return mCustomConfiguration;
+ }
+
+ public void setCustomConfiguration(String customConfiguration) {
+ this.mCustomConfiguration = customConfiguration;
+ }
+
+ public boolean isUseCustomConfig() {
+ return mUseCustomConfig;
+ }
+
+ public void setUseCustomConfig(boolean useCustomConfig) {
+ this.mUseCustomConfig = useCustomConfig;
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.mEnabled = enabled;
+ }
+
+ public int getConnectTimeout() {
+ return mConnectTimeout;
+ }
+
+ public void setConnectTimeout(int connectTimeout) {
+ this.mConnectTimeout = connectTimeout;
+ }
+
+ public ProxyType getProxyType() {
+ return mProxyType;
+ }
+
+ public void setProxyType(ProxyType proxyType) {
+ this.mProxyType = proxyType;
+ }
+
+ public String getProxyName() {
+ return mProxyName;
+ }
+
+ public void setProxyName(String proxyName) {
+ this.mProxyName = proxyName;
+ }
+
+ public String getProxyPort() {
+ return mProxyPort;
+ }
+
+ public void setProxyPort(String proxyPort) {
+ this.mProxyPort = proxyPort;
+ }
+
+ public boolean isUseProxyAuth() {
+ return mUseProxyAuth;
+ }
+
+ public void setUseProxyAuth(boolean useProxyAuth) {
+ this.mUseProxyAuth = useProxyAuth;
+ }
+
+ public String getProxyAuthUser() {
+ return mProxyAuthUser;
+ }
+
+ public void setProxyAuthUser(String proxyAuthUser) {
+ this.mProxyAuthUser = proxyAuthUser;
+ }
+
+ public String getProxyAuthPassword() {
+ return mProxyAuthPassword;
+ }
+
+ public void setProxyAuthPassword(String proxyAuthPassword) {
+ this.mProxyAuthPassword = proxyAuthPassword;
+ }
+
+ public abstract TransportType getTransportType();
+}
diff --git a/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java b/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java
new file mode 100644
index 00000000..a2f86e05
--- /dev/null
+++ b/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java
@@ -0,0 +1,59 @@
+package de.blinkt.openvpn.core.connection;
+
+import se.leap.bitmaskclient.pluggableTransports.Obfs4Options;
+
+import static se.leap.bitmaskclient.pluggableTransports.Dispatcher.DISPATCHER_IP;
+import static se.leap.bitmaskclient.pluggableTransports.Dispatcher.DISPATCHER_PORT;
+
+/**
+ * Created by cyberta on 08.03.19.
+ */
+
+public class Obfs4Connection extends Connection {
+
+ private static final String TAG = Obfs4Connection.class.getName();
+ private Obfs4Options options;
+
+ public Obfs4Connection(Obfs4Options options) {
+ setUseUdp(false);
+ setServerName(DISPATCHER_IP);
+ setServerPort(DISPATCHER_PORT);
+ setProxyName("");
+ setProxyPort("");
+ setProxyAuthUser(null);
+ setProxyAuthPassword(null);
+ setProxyType(ProxyType.NONE);
+ setUseProxyAuth(false);
+ this.options = options;
+ }
+
+ @Deprecated
+ public Obfs4Connection() {
+ setUseUdp(false);
+ setServerName(DISPATCHER_IP);
+ setServerPort(DISPATCHER_PORT);
+ setProxyName("");
+ setProxyPort("");
+ setProxyAuthUser(null);
+ setProxyAuthPassword(null);
+ setProxyType(ProxyType.NONE);
+ setUseProxyAuth(false); }
+
+ @Override
+ public Connection clone() throws CloneNotSupportedException {
+ Obfs4Connection connection = (Obfs4Connection) super.clone();
+ connection.options = this.options;
+ return connection;
+ }
+
+ @Override
+ public TransportType getTransportType() {
+ return TransportType.OBFS4;
+ }
+
+
+ public Obfs4Options getDispatcherOptions() {
+ return options;
+ }
+
+}
diff --git a/app/src/main/java/de/blinkt/openvpn/core/connection/OpenvpnConnection.java b/app/src/main/java/de/blinkt/openvpn/core/connection/OpenvpnConnection.java
new file mode 100644
index 00000000..3a3fd0c3
--- /dev/null
+++ b/app/src/main/java/de/blinkt/openvpn/core/connection/OpenvpnConnection.java
@@ -0,0 +1,13 @@
+package de.blinkt.openvpn.core.connection;
+
+/**
+ * Created by cyberta on 11.03.19.
+ */
+
+public class OpenvpnConnection extends Connection {
+
+ @Override
+ public TransportType getTransportType() {
+ return TransportType.OPENVPN;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/Constants.java b/app/src/main/java/se/leap/bitmaskclient/Constants.java
index 18338a73..720cd1c4 100644
--- a/app/src/main/java/se/leap/bitmaskclient/Constants.java
+++ b/app/src/main/java/se/leap/bitmaskclient/Constants.java
@@ -14,6 +14,7 @@ public interface Constants {
String CLEARLOG = "clearlogconnect";
String LAST_USED_PROFILE = "last_used_profile";
String EXCLUDED_APPS = "excluded_apps";
+ String USE_PLUGGABLE_TRANSPORTS = "usePluggableTransports";
//////////////////////////////////////////////
@@ -114,4 +115,22 @@ public interface Constants {
String FIRST_TIME_USER_DATE = "first_time_user_date";
+ //////////////////////////////////////////////
+ // JSON KEYS
+ /////////////////////////////////////////////
+ String IP_ADDRESS = "ip_address";
+ String REMOTE = "remote";
+ String PORTS = "ports";
+ String PROTOCOLS = "protocols";
+ String CAPABILITIES = "capabilities";
+ String TRANSPORT = "transport";
+ String TYPE = "type";
+ String OPTIONS = "options";
+ String VERSION = "version";
+ String NAME = "name";
+ String TIMEZONE = "timezone";
+ String LOCATIONS = "locations";
+ String LOCATION = "location";
+ String OPENVPN_CONFIGURATION = "openvpn_configuration";
+ String GATEWAYS = "gateways";
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/DrawerSettingsAdapter.java b/app/src/main/java/se/leap/bitmaskclient/DrawerSettingsAdapter.java
index 024bfaba..e69de29b 100644
--- a/app/src/main/java/se/leap/bitmaskclient/DrawerSettingsAdapter.java
+++ b/app/src/main/java/se/leap/bitmaskclient/DrawerSettingsAdapter.java
@@ -1,246 +0,0 @@
-/**
- * Copyright (c) 2018 LEAP Encryption Access Project and contributers
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-package se.leap.bitmaskclient;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.NonNull;
-import android.support.v7.widget.SwitchCompat;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-import android.widget.CompoundButton;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-
-/**
- * Created by cyberta on 21.02.18.
- */
-
-public class DrawerSettingsAdapter extends BaseAdapter {
-
- //item types
- public static final int NONE = -1;
- public static final int SWITCH_PROVIDER = 0;
- public static final int LOG = 1;
- public static final int ABOUT = 2;
- public static final int BATTERY_SAVER = 3;
- public static final int ALWAYS_ON = 4;
- public static final int DONATE = 5;
- public static final int SELECT_APPS = 6;
-
- //view types
- public final static int VIEW_SIMPLE_TEXT = 0;
- public final static int VIEW_SWITCH = 1;
-
- public static class DrawerSettingsItem {
- private String description = "";
- private int viewType = VIEW_SIMPLE_TEXT;
- private boolean isChecked = false;
- private int itemType = NONE;
- private CompoundButton.OnCheckedChangeListener callback;
- private Drawable iconResource;
-
- private DrawerSettingsItem(Context context, String description, @DrawableRes int iconResource, int viewType, boolean isChecked, int itemType, CompoundButton.OnCheckedChangeListener callback) {
- this.description = description;
- this.viewType = viewType;
- this.isChecked = isChecked;
- this.itemType = itemType;
- this.callback = callback;
- try {
- this.iconResource = context.getResources().getDrawable(iconResource);
- } catch (RuntimeException e) {
- e.printStackTrace();
- }
- }
-
- public static DrawerSettingsItem getSimpleTextInstance(Context context, String description, @DrawableRes int iconResource, int itemType) {
- return new DrawerSettingsItem(context, description, iconResource, VIEW_SIMPLE_TEXT, false, itemType, null);
- }
-
- public static DrawerSettingsItem getSwitchInstance(Context context, String description, @DrawableRes int iconResource, boolean isChecked, int itemType, CompoundButton.OnCheckedChangeListener callback) {
- return new DrawerSettingsItem(context, description, iconResource, VIEW_SWITCH, isChecked, itemType, callback);
- }
-
- public int getItemType() {
- return itemType;
- }
-
- public void setChecked(boolean checked) {
- isChecked = checked;
- }
-
- public boolean isChecked() {
- return isChecked;
- }
- }
-
- private ArrayList<DrawerSettingsItem> mData = new ArrayList<>();
- private LayoutInflater mInflater;
-
- public DrawerSettingsAdapter(LayoutInflater layoutInflater) {
- mInflater = layoutInflater;
- }
-
- public void addItem(final DrawerSettingsItem item) {
- mData.add(item);
- notifyDataSetChanged();
- }
-
- @Override
- public int getItemViewType(int position) {
- DrawerSettingsItem item = mData.get(position);
- return item.viewType;
- }
-
- @Override
- public int getViewTypeCount() {
- boolean hasSwitchItem = false;
- for (DrawerSettingsItem item : mData) {
- if (item.viewType == VIEW_SWITCH) {
- hasSwitchItem = true;
- break;
- }
- }
- return hasSwitchItem ? 2 : 1;
- }
-
- @Override
- public int getCount() {
- return mData.size();
- }
-
- @Override
- public DrawerSettingsItem getItem(int position) {
- return mData.get(position);
- }
-
- @Override
- public long getItemId(int position) {
- return position;
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
-
- DrawerSettingsItem drawerSettingsItem = mData.get(position);
- ViewHolder holder = null;
- int type = getItemViewType(position);
- if (convertView == null) {
- holder = new ViewHolder();
- switch(type) {
- case VIEW_SIMPLE_TEXT:
- convertView = initTextViewBinding(holder);
- bindSimpleText(drawerSettingsItem, holder);
- break;
- case VIEW_SWITCH:
- convertView = initSwitchBinding(holder);
- bindSwitch(drawerSettingsItem, holder);
- break;
- }
- convertView.setTag(holder);
- } else {
- holder = (ViewHolder)convertView.getTag();
- switch (type) {
- case VIEW_SIMPLE_TEXT:
- if (holder.isSwitchViewHolder()) {
- holder.resetSwitchView();
- convertView = initTextViewBinding(holder);
- }
- bindSimpleText(drawerSettingsItem, holder);
- break;
- case VIEW_SWITCH:
- if (!holder.isSwitchViewHolder()) {
- holder.resetTextView();
- convertView = initSwitchBinding(holder);
- }
- bindSwitch(drawerSettingsItem, holder);
- break;
- }
- convertView.setTag(holder);
- }
- return convertView;
- }
-
- private void bindSimpleText(DrawerSettingsItem drawerSettingsItem, ViewHolder holder) {
- holder.textView.setText(drawerSettingsItem.description);
- if (drawerSettingsItem.iconResource != null) {
- holder.iconView.setImageDrawable(drawerSettingsItem.iconResource);
- }
- }
-
- private void bindSwitch(DrawerSettingsItem drawerSettingsItem, ViewHolder holder) {
- holder.switchView.setChecked(drawerSettingsItem.isChecked);
- holder.textView.setText(drawerSettingsItem.description);
- holder.switchView.setOnCheckedChangeListener(drawerSettingsItem.callback);
- if (drawerSettingsItem.iconResource != null) {
- holder.iconView.setImageDrawable(drawerSettingsItem.iconResource);
- }
- }
-
- @NonNull
- private View initSwitchBinding(ViewHolder holder) {
- View convertView = mInflater.inflate(R.layout.v_switch_list_item, null);
- holder.switchView = convertView.findViewById(R.id.option_switch);
- holder.textView = convertView.findViewById(android.R.id.text1);
- holder.iconView = convertView.findViewById(R.id.material_icon);
- return convertView;
- }
-
- @NonNull
- private View initTextViewBinding(ViewHolder holder) {
- View convertView = mInflater.inflate(R.layout.v_icon_text_list_item, null);
- holder.textView = convertView.findViewById(android.R.id.text1);
- holder.iconView = convertView.findViewById(R.id.material_icon);
- return convertView;
- }
-
- public DrawerSettingsItem getDrawerItem(int elementType) {
- for (DrawerSettingsItem item : mData) {
- if (item.itemType == elementType) {
- return item;
- }
- }
- return null;
- }
-
- static class ViewHolder {
- TextView textView;
- ImageView iconView;
- SwitchCompat switchView;
-
- boolean isSwitchViewHolder() {
- return switchView != null;
- }
-
- void resetSwitchView() {
- switchView.setOnCheckedChangeListener(null);
- switchView = null;
- }
-
- void resetTextView() {
- textView = null;
- }
- }
-}
-
-
-
diff --git a/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java b/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java
index a8aa2dfb..7327c416 100644
--- a/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java
+++ b/app/src/main/java/se/leap/bitmaskclient/EipSetupObserver.java
@@ -168,6 +168,7 @@ class EipSetupObserver extends BroadcastReceiver implements VpnStatus.StateListe
if (resultCode == RESULT_CANCELED) {
//setup failed
finishGatewaySetup(false);
+ EipStatus.refresh();
}
break;
default:
diff --git a/app/src/main/java/se/leap/bitmaskclient/Provider.java b/app/src/main/java/se/leap/bitmaskclient/Provider.java
index c81f5739..067f9b2e 100644
--- a/app/src/main/java/se/leap/bitmaskclient/Provider.java
+++ b/app/src/main/java/se/leap/bitmaskclient/Provider.java
@@ -21,6 +21,7 @@ import android.os.Parcelable;
import com.google.gson.Gson;
+import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -28,8 +29,13 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.util.Locale;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
+import static se.leap.bitmaskclient.Constants.CAPABILITIES;
+import static se.leap.bitmaskclient.Constants.GATEWAYS;
import static se.leap.bitmaskclient.Constants.PROVIDER_ALLOWED_REGISTERED;
import static se.leap.bitmaskclient.Constants.PROVIDER_ALLOW_ANONYMOUS;
+import static se.leap.bitmaskclient.Constants.TRANSPORT;
+import static se.leap.bitmaskclient.Constants.TYPE;
import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
/**
@@ -119,6 +125,25 @@ public final class Provider implements Parcelable {
hasPrivateKey();
}
+ public boolean supportsPluggableTransports() {
+ try {
+ JSONArray gatewayJsons = eipServiceJson.getJSONArray(GATEWAYS);
+ for (int i = 0; i < gatewayJsons.length(); i++) {
+ JSONArray transports = gatewayJsons.getJSONObject(i).
+ getJSONObject(CAPABILITIES).
+ getJSONArray(TRANSPORT);
+ for (int j = 0; j < transports.length(); j++) {
+ if (OBFS4.toString().equals(transports.getJSONObject(j).getString(TYPE))) {
+ return true;
+ }
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return false;
+ }
+
public void setMainUrl(URL url) {
mainUrl.setUrl(url);
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java
index 37adbe93..46782802 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java
@@ -49,6 +49,7 @@ import java.util.List;
import java.util.NoSuchElementException;
import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
import okhttp3.OkHttpClient;
import se.leap.bitmaskclient.Constants.CREDENTIAL_ERRORS;
@@ -578,7 +579,7 @@ public abstract class ProviderApiManagerBase {
plainResponseBody = formatErrorMessage(server_unreachable_message);
} catch (MalformedURLException e) {
plainResponseBody = formatErrorMessage(malformed_url);
- } catch (SSLHandshakeException e) {
+ } catch (SSLHandshakeException | SSLPeerUnverifiedException e) {
plainResponseBody = formatErrorMessage(certificate_error);
} catch (ConnectException e) {
plainResponseBody = formatErrorMessage(service_is_down_error);
@@ -750,6 +751,13 @@ public abstract class ProviderApiManagerBase {
return result;
}
+ protected Bundle setErrorResult(Bundle result, String stringJsonErrorMessage) {
+ String reasonToFail = pickErrorMessage(stringJsonErrorMessage);
+ result.putString(ERRORS, reasonToFail);
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+ return result;
+ }
+
Bundle setErrorResult(Bundle result, int errorMessageId, String errorId) {
JSONObject errorJson = new JSONObject();
String errorMessage = getProviderFormattedString(resources, errorMessageId);
diff --git a/app/src/main/java/se/leap/bitmaskclient/StartActivity.java b/app/src/main/java/se/leap/bitmaskclient/StartActivity.java
index d8aca351..b89363b2 100644
--- a/app/src/main/java/se/leap/bitmaskclient/StartActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/StartActivity.java
@@ -162,8 +162,8 @@ public class StartActivity extends Activity{
}
private void prepareEIP() {
- boolean provider_exists = providerInSharedPreferences(preferences);
- if (provider_exists) {
+ boolean providerExists = providerInSharedPreferences(preferences);
+ if (providerExists) {
Provider provider = getSavedProviderFromSharedPreferences(preferences);
if(!provider.isConfigured()) {
configureLeapProvider();
@@ -215,5 +215,4 @@ public class StartActivity extends Activity{
startActivity(intent);
finish();
}
-
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/VpnNotificationManager.java b/app/src/main/java/se/leap/bitmaskclient/VpnNotificationManager.java
index 9107568c..b276a402 100644
--- a/app/src/main/java/se/leap/bitmaskclient/VpnNotificationManager.java
+++ b/app/src/main/java/se/leap/bitmaskclient/VpnNotificationManager.java
@@ -24,11 +24,17 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
+import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.style.StyleSpan;
import android.widget.RemoteViews;
import de.blinkt.openvpn.LaunchVPN;
@@ -43,8 +49,8 @@ import static android.support.v4.app.NotificationCompat.PRIORITY_MIN;
import static android.text.TextUtils.isEmpty;
import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_NONETWORK;
import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT;
-import static se.leap.bitmaskclient.Constants.EIP_ACTION_STOP_BLOCKING_VPN;
import static se.leap.bitmaskclient.Constants.ASK_TO_CANCEL_VPN;
+import static se.leap.bitmaskclient.Constants.EIP_ACTION_STOP_BLOCKING_VPN;
import static se.leap.bitmaskclient.MainActivity.ACTION_SHOW_VPN_FRAGMENT;
/**
@@ -83,6 +89,7 @@ public class VpnNotificationManager {
buildVpnNotification(
context.getString(R.string.void_vpn_title),
msg,
+ null,
tickerText,
status,
VoidVpnService.NOTIFICATION_CHANNEL_NEWSTATUS_ID,
@@ -110,8 +117,11 @@ public class VpnNotificationManager {
* @param status
* @param when
*/
- public void buildOpenVpnNotification(String profileName, final String msg, String tickerText, ConnectionStatus status, long when, String notificationChannelNewstatusId) {
+ public void buildOpenVpnNotification(String profileName, boolean isObfuscated, String msg, String tickerText, ConnectionStatus status, long when, String notificationChannelNewstatusId) {
String cancelString;
+ CharSequence bigmessage = null;
+ String ghostIcon = new String(Character.toChars(0x1f309));
+
switch (status) {
// show cancel if no connection
case LEVEL_START:
@@ -119,11 +129,28 @@ public class VpnNotificationManager {
case LEVEL_CONNECTING_SERVER_REPLIED:
case LEVEL_CONNECTING_NO_SERVER_REPLY_YET:
cancelString = context.getString(R.string.cancel);
+ if (isObfuscated && Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
+ Spannable spannable = new SpannableString(context.getString(R.string.obfuscated_connection_try));
+ spannable.setSpan(new StyleSpan(Typeface.ITALIC), 0, spannable.length() -1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ bigmessage = TextUtils.concat(spannable, " " + ghostIcon + "\n" + msg);
+ }
break;
+
// show disconnect if connection exists
+ case LEVEL_CONNECTED:
+ if (isObfuscated && Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
+ Spannable spannable = new SpannableString(context.getString(R.string.obfuscated_connection));
+ spannable.setSpan(new StyleSpan(Typeface.ITALIC), 0, spannable.length() -1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ bigmessage = TextUtils.concat(spannable, " " + ghostIcon + "\n" + msg);
+ }
default:
cancelString = context.getString(R.string.cancel_connection);
}
+
+ if (isObfuscated) {
+ msg = ghostIcon + " " + msg;
+ }
+
NotificationCompat.Action.Builder actionBuilder = new NotificationCompat.Action.
Builder(R.drawable.ic_menu_close_clear_cancel, cancelString, getDisconnectIntent());
String title;
@@ -151,6 +178,7 @@ public class VpnNotificationManager {
buildVpnNotification(
title,
msg,
+ bigmessage,
tickerText,
status,
notificationChannelNewstatusId,
@@ -224,28 +252,30 @@ public class VpnNotificationManager {
return remoteViews;
}
- private void buildVpnNotification(String title, final String msg, String tickerText, ConnectionStatus status, String notificationChannelNewstatusId, int priority, long when, PendingIntent contentIntent, NotificationCompat.Action notificationAction) {
+ private void buildVpnNotification(String title, String message, CharSequence bigMessage, String tickerText, ConnectionStatus status, String notificationChannelNewstatusId, int priority, long when, PendingIntent contentIntent, NotificationCompat.Action notificationAction) {
NotificationCompat.Builder nCompatBuilder = new NotificationCompat.Builder(context, notificationChannelNewstatusId);
int icon = getIconByConnectionStatus(status);
// this is a workaround to avoid confusion between the Android's system vpn notification
// showing a filled out key icon and the bitmask icon indicating a different state.
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT &&
- notificationChannelNewstatusId.equals(OpenVPNService.NOTIFICATION_CHANNEL_NEWSTATUS_ID) &&
- status != LEVEL_NONETWORK
- ) {
- // removes the icon from the system status bar
- icon = android.R.color.transparent;
- // adds the icon to the notification in the notification drawer
- nCompatBuilder.setContent(getKitkatCustomRemoteView(status, title, msg));
+ notificationChannelNewstatusId.equals(OpenVPNService.NOTIFICATION_CHANNEL_NEWSTATUS_ID)) {
+ if (status != LEVEL_NONETWORK) {
+ // removes the icon from the system status bar
+ icon = android.R.color.transparent;
+ // adds the icon to the notification in the notification drawer
+ nCompatBuilder.setContent(getKitkatCustomRemoteView(status, title, message));
+ }
} else {
- nCompatBuilder.addAction(notificationAction);
+ nCompatBuilder.setStyle(new NotificationCompat.BigTextStyle().
+ setBigContentTitle(title).
+ bigText(bigMessage));
}
-
+ nCompatBuilder.addAction(notificationAction);
nCompatBuilder.setContentTitle(title);
nCompatBuilder.setCategory(NotificationCompat.CATEGORY_SERVICE);
nCompatBuilder.setLocalOnly(true);
- nCompatBuilder.setContentText(msg);
+ nCompatBuilder.setContentText(message);
nCompatBuilder.setOnlyAlertOnce(true);
nCompatBuilder.setSmallIcon(icon);
nCompatBuilder.setPriority(priority);
diff --git a/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java b/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java
index a604c536..e3c7ac1b 100644
--- a/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/drawer/NavigationDrawerFragment.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ * Copyright (c) 2019 LEAP Encryption Access Project and contributers
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,7 +18,6 @@ package se.leap.bitmaskclient.drawer;
import android.app.Activity;
-import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
@@ -44,50 +43,41 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
-import android.widget.ListView;
-import se.leap.bitmaskclient.DrawerSettingsAdapter;
-import se.leap.bitmaskclient.DrawerSettingsAdapter.DrawerSettingsItem;
+import de.blinkt.openvpn.core.VpnStatus;
import se.leap.bitmaskclient.EipFragment;
import se.leap.bitmaskclient.FragmentManagerEnhanced;
import se.leap.bitmaskclient.MainActivity;
import se.leap.bitmaskclient.Provider;
import se.leap.bitmaskclient.ProviderListActivity;
+import se.leap.bitmaskclient.ProviderObservable;
import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.eip.EipCommand;
import se.leap.bitmaskclient.fragments.AboutFragment;
import se.leap.bitmaskclient.fragments.AlwaysOnDialog;
-import se.leap.bitmaskclient.fragments.LogFragment;
import se.leap.bitmaskclient.fragments.ExcludeAppsFragment;
+import se.leap.bitmaskclient.fragments.LogFragment;
+import se.leap.bitmaskclient.views.IconSwitchEntry;
+import se.leap.bitmaskclient.views.IconTextEntry;
import static android.content.Context.MODE_PRIVATE;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
import static se.leap.bitmaskclient.BitmaskApp.getRefWatcher;
import static se.leap.bitmaskclient.Constants.DONATION_URL;
import static se.leap.bitmaskclient.Constants.ENABLE_DONATION;
import static se.leap.bitmaskclient.Constants.PROVIDER_KEY;
import static se.leap.bitmaskclient.Constants.REQUEST_CODE_SWITCH_PROVIDER;
import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES;
-import static se.leap.bitmaskclient.DrawerSettingsAdapter.ABOUT;
-import static se.leap.bitmaskclient.DrawerSettingsAdapter.ALWAYS_ON;
-import static se.leap.bitmaskclient.DrawerSettingsAdapter.BATTERY_SAVER;
-import static se.leap.bitmaskclient.DrawerSettingsAdapter.DONATE;
-import static se.leap.bitmaskclient.DrawerSettingsAdapter.DrawerSettingsItem.getSimpleTextInstance;
-import static se.leap.bitmaskclient.DrawerSettingsAdapter.DrawerSettingsItem.getSwitchInstance;
-import static se.leap.bitmaskclient.DrawerSettingsAdapter.LOG;
-import static se.leap.bitmaskclient.DrawerSettingsAdapter.SELECT_APPS;
-import static se.leap.bitmaskclient.DrawerSettingsAdapter.SWITCH_PROVIDER;
import static se.leap.bitmaskclient.R.string.about_fragment_title;
import static se.leap.bitmaskclient.R.string.exclude_apps_fragment_title;
-import static se.leap.bitmaskclient.R.string.donate_title;
import static se.leap.bitmaskclient.R.string.log_fragment_title;
-import static se.leap.bitmaskclient.R.string.switch_provider_menu_option;
import static se.leap.bitmaskclient.utils.ConfigHelper.isDefaultBitmask;
-import static se.leap.bitmaskclient.utils.PreferenceHelper.getProviderName;
import static se.leap.bitmaskclient.utils.PreferenceHelper.getSaveBattery;
-import static se.leap.bitmaskclient.utils.PreferenceHelper.getSavedProviderFromSharedPreferences;
import static se.leap.bitmaskclient.utils.PreferenceHelper.getShowAlwaysOnDialog;
+import static se.leap.bitmaskclient.utils.PreferenceHelper.getUsePluggableTransports;
import static se.leap.bitmaskclient.utils.PreferenceHelper.saveBattery;
+import static se.leap.bitmaskclient.utils.PreferenceHelper.usePluggableTransports;
/**
* Fragment used for managing interactions for and presentation of a navigation drawer.
@@ -112,11 +102,10 @@ public class NavigationDrawerFragment extends Fragment {
private DrawerLayout drawerLayout;
private View drawerView;
- private ListView drawerAccountsListView;
private View fragmentContainerView;
- private ArrayAdapter<String> accountListAdapter;
- private DrawerSettingsAdapter settingsListAdapter;
private Toolbar toolbar;
+ private IconTextEntry account;
+ private IconSwitchEntry saveBattery;
private boolean userLearnedDrawer;
private volatile boolean wasPaused;
@@ -186,14 +175,8 @@ public class NavigationDrawerFragment extends Fragment {
this.drawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
toolbar = this.drawerLayout.findViewById(R.id.toolbar);
- final ActionBar actionBar = setupActionBar();
- setupSettingsListAdapter();
- setupSettingsListView();
- accountListAdapter = new ArrayAdapter<>(actionBar.getThemedContext(),
- R.layout.v_icon_text_list_item,
- android.R.id.text1);
- refreshAccountListAdapter();
- setupAccountsListView();
+ setupActionBar();
+ setupEntries();
setupActionBarDrawerToggle(activity);
if (!userLearnedDrawer) {
@@ -243,40 +226,144 @@ public class NavigationDrawerFragment extends Fragment {
};
}
- private void setupAccountsListView() {
- drawerAccountsListView = drawerView.findViewById(R.id.accountList);
- drawerAccountsListView.setAdapter(accountListAdapter);
- drawerAccountsListView.setOnItemClickListener((parent, view, position, id) -> selectItem(parent, position));
+ private void setupEntries() {
+ initAccountEntry();
+ initSwitchProviderEntry();
+ initUseBridgesEntry();
+ initSaveBatteryEntry();
+ initAlwaysOnVpnEntry();
+ initExcludeAppsEntry();
+ initDonateEntry();
+ initLogEntry();
+ initAboutEntry();
+ }
+
+ private void initAccountEntry() {
+ account = drawerView.findViewById(R.id.account);
+ FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
+ Provider currentProvider = ProviderObservable.getInstance().getCurrentProvider();
+ account.setText(currentProvider.getName());
+ account.setOnClickListener((buttonView) -> {
+ Fragment fragment = new EipFragment();
+ Bundle arguments = new Bundle();
+ arguments.putParcelable(PROVIDER_KEY, currentProvider);
+ fragment.setArguments(arguments);
+ hideActionBarSubTitle();
+ fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
+ closeDrawer();
+ });
}
- private void setupSettingsListView() {
- ListView drawerSettingsListView = drawerView.findViewById(R.id.settingsList);
- drawerSettingsListView.setOnItemClickListener((parent, view, position, id) -> selectItem(parent, position));
- drawerSettingsListView.setAdapter(settingsListAdapter);
+ private void initSwitchProviderEntry() {
+ if (isDefaultBitmask()) {
+ IconTextEntry switchProvider = drawerView.findViewById(R.id.switch_provider);
+ switchProvider.setVisibility(VISIBLE);
+ switchProvider.setOnClickListener(v ->
+ getActivity().startActivityForResult(new Intent(getActivity(), ProviderListActivity.class), REQUEST_CODE_SWITCH_PROVIDER));
+ }
}
- private void setupSettingsListAdapter() {
- settingsListAdapter = new DrawerSettingsAdapter(getLayoutInflater());
- if (getContext() != null) {
- settingsListAdapter.addItem(getSwitchInstance(getContext(),
- getString(R.string.save_battery),
- R.drawable.ic_battery_36,
- getSaveBattery(getContext()),
- BATTERY_SAVER,
- (buttonView, newStateIsChecked) -> onSwitchItemSelected(BATTERY_SAVER, newStateIsChecked)));
+ private void initUseBridgesEntry() {
+ IconSwitchEntry useBridges = drawerView.findViewById(R.id.bridges_switch);
+ if (ProviderObservable.getInstance().getCurrentProvider().supportsPluggableTransports()) {
+ useBridges.setVisibility(VISIBLE);
+ useBridges.setChecked(getUsePluggableTransports(getContext()));
+ useBridges.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ usePluggableTransports(getContext(), isChecked);
+ if (VpnStatus.isVPNActive()) {
+ EipCommand.startVPN(getContext(), true);
+ closeDrawer();
+ }
+ });
+
+
+ } else {
+ useBridges.setVisibility(GONE);
}
+ }
+
+ private void initSaveBatteryEntry() {
+ saveBattery = drawerView.findViewById(R.id.battery_switch);
+ saveBattery.setChecked(getSaveBattery(getContext()));
+ saveBattery.setOnCheckedChangeListener(((buttonView, isChecked) -> {
+ if (isChecked) {
+ showExperimentalFeatureAlert();
+ } else {
+ saveBattery(getContext(), false);
+ }
+ }));
+ }
+
+ private void initAlwaysOnVpnEntry() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- settingsListAdapter.addItem(getSimpleTextInstance(getContext(), getString(R.string.always_on_vpn), R.drawable.ic_always_on_36, ALWAYS_ON));
+ IconTextEntry alwaysOnVpn = drawerView.findViewById(R.id.always_on_vpn);
+ alwaysOnVpn.setVisibility(VISIBLE);
+ alwaysOnVpn.setOnClickListener((buttonView) -> {
+ closeDrawer();
+ if (getShowAlwaysOnDialog(getContext())) {
+ showAlwaysOnDialog();
+ } else {
+ Intent intent = new Intent("android.net.vpn.SETTINGS");
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ }
+ });
}
- if (isDefaultBitmask()) {
- settingsListAdapter.addItem(getSimpleTextInstance(getContext(), getString(switch_provider_menu_option), R.drawable.ic_switch_provider_36, SWITCH_PROVIDER));
+ }
+
+ private void initExcludeAppsEntry() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ IconTextEntry excludeApps = drawerView.findViewById(R.id.exclude_apps);
+ excludeApps.setVisibility(VISIBLE);
+ FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
+ excludeApps.setOnClickListener((buttonView) -> {
+ closeDrawer();
+ Fragment fragment = new ExcludeAppsFragment();
+ setActionBarTitle(exclude_apps_fragment_title);
+ fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
+ });
}
- settingsListAdapter.addItem(getSimpleTextInstance(getContext(), getString(exclude_apps_fragment_title), R.drawable.ic_shield_remove_grey600_36dp, SELECT_APPS));
- settingsListAdapter.addItem(getSimpleTextInstance(getContext(), getString(log_fragment_title), R.drawable.ic_log_36, LOG));
+ }
+
+ private void initDonateEntry() {
if (ENABLE_DONATION) {
- settingsListAdapter.addItem(getSimpleTextInstance(getContext(), getString(donate_title), R.drawable.ic_donate_36, DONATE));
+ IconTextEntry donate = drawerView.findViewById(R.id.donate);
+ donate.setVisibility(VISIBLE);
+ donate.setOnClickListener((buttonView) -> {
+ closeDrawer();
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(DONATION_URL));
+ startActivity(browserIntent);
+
+ });
+ }
+ }
+
+ private void initLogEntry() {
+ IconTextEntry log = drawerView.findViewById(R.id.log);
+ FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
+ log.setOnClickListener((buttonView) -> {
+ closeDrawer();
+ Fragment fragment = new LogFragment();
+ setActionBarTitle(log_fragment_title);
+ fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
+ });
+ }
+
+ private void initAboutEntry() {
+ IconTextEntry about = drawerView.findViewById(R.id.about);
+ FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
+ about.setOnClickListener((buttonView) -> {
+ closeDrawer();
+ Fragment fragment = new AboutFragment();
+ setActionBarTitle(about_fragment_title);
+ fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
+ });
+ }
+
+ private void closeDrawer() {
+ if (drawerLayout != null) {
+ drawerLayout.closeDrawer(fragmentContainerView);
}
- settingsListAdapter.addItem(getSimpleTextInstance(getContext(), getString(about_fragment_title), R.drawable.ic_about_36, ABOUT));
}
private ActionBar setupActionBar() {
@@ -324,16 +411,6 @@ public class NavigationDrawerFragment extends Fragment {
}, TWO_SECONDS);
}
- private void selectItem(AdapterView<?> list, int position) {
- if (list != null) {
- ((ListView) list).setItemChecked(position, true);
- }
- if (drawerLayout != null) {
- drawerLayout.closeDrawer(fragmentContainerView);
- }
- onTextItemSelected(list, position);
- }
-
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
@@ -361,17 +438,11 @@ public class NavigationDrawerFragment extends Fragment {
.setTitle(activity.getString(R.string.save_battery))
.setMessage(activity.getString(R.string.save_battery_message))
.setPositiveButton((android.R.string.yes), (dialog, which) -> {
- DrawerSettingsItem item = settingsListAdapter.getDrawerItem(BATTERY_SAVER);
- item.setChecked(true);
- settingsListAdapter.notifyDataSetChanged();
- saveBattery(getContext(), item.isChecked());
+ saveBattery(getContext(), true);
})
- .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> disableSwitch(BATTERY_SAVER)).setOnDismissListener(new DialogInterface.OnDismissListener() {
- @Override
- public void onDismiss(DialogInterface dialog) {
- showEnableExperimentalFeature = false;
- }
- }).setOnCancelListener(dialog -> disableSwitch(BATTERY_SAVER)).show();
+ .setNegativeButton(activity.getString(android.R.string.no), (dialog, which) -> saveBattery.setCheckedQuietly(false))
+ .setOnDismissListener(dialog -> showEnableExperimentalFeature = false)
+ .setOnCancelListener(dialog -> saveBattery.setCheckedQuietly(false)).show();
} catch (IllegalStateException e) {
e.printStackTrace();
}
@@ -434,85 +505,6 @@ public class NavigationDrawerFragment extends Fragment {
return ((AppCompatActivity) getActivity()).getSupportActionBar();
}
- private void onSwitchItemSelected(int elementType, boolean newStateIsChecked) {
- switch (elementType) {
- case BATTERY_SAVER:
- if (getSaveBattery(getContext()) == newStateIsChecked) {
- //initial ui setup, ignore
- return;
- }
- if (newStateIsChecked) {
- showExperimentalFeatureAlert();
- } else {
- saveBattery(this.getContext(), false);
- disableSwitch(BATTERY_SAVER);
- }
- break;
- default:
- break;
- }
- }
-
- private void disableSwitch(int elementType) {
- DrawerSettingsItem item = settingsListAdapter.getDrawerItem(elementType);
- item.setChecked(false);
- settingsListAdapter.notifyDataSetChanged();
- }
-
- public void onTextItemSelected(AdapterView<?> parent, int position) {
- // update the main content by replacing fragments
- FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
- Fragment fragment = null;
-
- if (parent == drawerAccountsListView) {
- fragment = new EipFragment();
- Bundle arguments = new Bundle();
- Provider currentProvider = getSavedProviderFromSharedPreferences(preferences);
- arguments.putParcelable(PROVIDER_KEY, currentProvider);
- fragment.setArguments(arguments);
- hideActionBarSubTitle();
- } else {
- DrawerSettingsItem settingsItem = settingsListAdapter.getItem(position);
- switch (settingsItem.getItemType()) {
- case SWITCH_PROVIDER:
- getActivity().startActivityForResult(new Intent(getActivity(), ProviderListActivity.class), REQUEST_CODE_SWITCH_PROVIDER);
- break;
- case LOG:
- fragment = new LogFragment();
- setActionBarTitle(log_fragment_title);
- break;
- case ABOUT:
- fragment = new AboutFragment();
- setActionBarTitle(about_fragment_title);
- break;
- case ALWAYS_ON:
- if (getShowAlwaysOnDialog(getContext())) {
- showAlwaysOnDialog();
- } else {
- Intent intent = new Intent("android.net.vpn.SETTINGS");
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- }
- break;
- case DONATE:
- Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(DONATION_URL));
- startActivity(browserIntent);
- break;
- case SELECT_APPS:
- fragment = new ExcludeAppsFragment();
- setActionBarTitle(exclude_apps_fragment_title);
- break;
- default:
- break;
- }
- }
-
- if (fragment != null) {
- fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
- }
-
- }
-
private void setActionBarTitle(@StringRes int resId) {
ActionBar actionBar = getActionBar();
if (actionBar != null) {
@@ -527,22 +519,10 @@ public class NavigationDrawerFragment extends Fragment {
}
}
-
public void refresh() {
- refreshAccountListAdapter();
- accountListAdapter.notifyDataSetChanged();
- drawerAccountsListView.setAdapter(accountListAdapter);
- }
-
- private void refreshAccountListAdapter() {
- accountListAdapter.clear();
- String providerName = getProviderName(preferences);
- if (providerName == null) {
- //TODO: ADD A header to the ListView containing a useful message.
- //TODO 2: disable switchProvider
- } else {
- accountListAdapter.add(providerName);
- }
+ Provider currentProvider = ProviderObservable.getInstance().getCurrentProvider();
+ account.setText(currentProvider.getName());
+ initUseBridgesEntry();
}
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java
index a5434871..19c539e8 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java
@@ -43,16 +43,20 @@ import java.util.Observer;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
+import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.ConnectionStatus;
import de.blinkt.openvpn.core.IOpenVPNServiceInternal;
import de.blinkt.openvpn.core.OpenVPNService;
import de.blinkt.openvpn.core.VpnStatus;
+import de.blinkt.openvpn.core.connection.Connection;
import se.leap.bitmaskclient.OnBootReceiver;
import se.leap.bitmaskclient.R;
import static android.app.Activity.RESULT_CANCELED;
import static android.app.Activity.RESULT_OK;
import static android.content.Intent.CATEGORY_DEFAULT;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN;
import static se.leap.bitmaskclient.Constants.BROADCAST_EIP_EVENT;
import static se.leap.bitmaskclient.Constants.BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT;
import static se.leap.bitmaskclient.Constants.BROADCAST_RESULT_CODE;
@@ -74,6 +78,7 @@ import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES;
import static se.leap.bitmaskclient.MainActivityErrorDialog.DOWNLOAD_ERRORS.ERROR_INVALID_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.R.string.vpn_certificate_is_invalid;
import static se.leap.bitmaskclient.utils.ConfigHelper.ensureNotOnMainThread;
+import static se.leap.bitmaskclient.utils.PreferenceHelper.getUsePluggableTransports;
/**
* EIP is the abstract base class for interacting with and managing the Encrypted
@@ -203,11 +208,11 @@ public final class EIP extends JobIntentService implements Observer {
GatewaysManager gatewaysManager = gatewaysFromPreferences();
Gateway gateway = gatewaysManager.select(nClosestGateway);
- if (gateway != null && gateway.getProfile() != null) {
- launchActiveGateway(gateway, nClosestGateway);
+ if (launchActiveGateway(gateway, nClosestGateway)) {
tellToReceiverOrBroadcast(EIP_ACTION_START, RESULT_OK);
- } else
+ } else {
tellToReceiverOrBroadcast(EIP_ACTION_START, RESULT_CANCELED);
+ }
}
/**
@@ -218,9 +223,7 @@ public final class EIP extends JobIntentService implements Observer {
GatewaysManager gatewaysManager = gatewaysFromPreferences();
Gateway gateway = gatewaysManager.select(0);
- if (gateway != null && gateway.getProfile() != null) {
- launchActiveGateway(gateway, 0);
- } else {
+ if (!launchActiveGateway(gateway, 0)) {
Log.d(TAG, "startEIPAlwaysOnVpn no active profile available!");
}
}
@@ -240,11 +243,19 @@ public final class EIP extends JobIntentService implements Observer {
*
* @param gateway to connect to
*/
- private void launchActiveGateway(@NonNull Gateway gateway, int nClosestGateway) {
+ private boolean launchActiveGateway(Gateway gateway, int nClosestGateway) {
+ VpnProfile profile;
+ Connection.TransportType transportType = getUsePluggableTransports(this) ? OBFS4 : OPENVPN;
+ if (gateway == null ||
+ (profile = gateway.getProfile(transportType)) == null) {
+ return false;
+ }
+
Intent intent = new Intent(BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT);
- intent.putExtra(PROVIDER_PROFILE, gateway.getProfile());
+ intent.putExtra(PROVIDER_PROFILE, profile);
intent.putExtra(Gateway.KEY_N_CLOSEST_GATEWAY, nClosestGateway);
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
+ return true;
}
@@ -277,7 +288,7 @@ public final class EIP extends JobIntentService implements Observer {
* @return GatewaysManager
*/
private GatewaysManager gatewaysFromPreferences() {
- GatewaysManager gatewaysManager = new GatewaysManager(this, preferences);
+ GatewaysManager gatewaysManager = new GatewaysManager(preferences);
gatewaysManager.configureFromPreferences();
return gatewaysManager;
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EipStatus.java b/app/src/main/java/se/leap/bitmaskclient/eip/EipStatus.java
index 64904816..69fc483a 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/EipStatus.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/EipStatus.java
@@ -78,8 +78,7 @@ public class EipStatus extends Observable implements VpnStatus.StateListener {
currentStatus.setLevel(level);
currentStatus.setEipLevel(level);
if (tmp != currentStatus.getLevel() || "RECONNECTING".equals(state)) {
- currentStatus.setChanged();
- currentStatus.notifyObservers();
+ refresh();
}
}
@@ -174,8 +173,7 @@ public class EipStatus extends Observable implements VpnStatus.StateListener {
default:
break;
}
- currentStatus.setChanged();
- currentStatus.notifyObservers();
+ refresh();
}
}
}
@@ -286,4 +284,9 @@ public class EipStatus extends Observable implements VpnStatus.StateListener {
return "State: " + state + " Level: " + vpnLevel.toString();
}
+ public static void refresh() {
+ currentStatus.setChanged();
+ currentStatus.notifyObservers();
+ }
+
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java b/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java
index 09b33845..15ee13c2 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java
@@ -17,7 +17,8 @@
package se.leap.bitmaskclient.eip;
import android.content.Context;
-import android.content.SharedPreferences;
+
+import android.support.annotation.NonNull;
import com.google.gson.Gson;
@@ -25,14 +26,22 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
-import java.io.StringReader;
import java.util.HashSet;
import java.util.Set;
+import java.util.HashMap;
import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.ConfigParser;
-import se.leap.bitmaskclient.BitmaskApp;
import se.leap.bitmaskclient.utils.PreferenceHelper;
+import de.blinkt.openvpn.core.connection.Connection;
+
+import static se.leap.bitmaskclient.Constants.IP_ADDRESS;
+import static se.leap.bitmaskclient.Constants.LOCATION;
+import static se.leap.bitmaskclient.Constants.LOCATIONS;
+import static se.leap.bitmaskclient.Constants.NAME;
+import static se.leap.bitmaskclient.Constants.OPENVPN_CONFIGURATION;
+import static se.leap.bitmaskclient.Constants.TIMEZONE;
+import static se.leap.bitmaskclient.Constants.VERSION;
/**
* Gateway provides objects defining gateways and their metadata.
@@ -41,6 +50,7 @@ import se.leap.bitmaskclient.utils.PreferenceHelper;
*
* @author Sean Leonard <meanderingcode@aetherislands.net>
* @author Parménides GV <parmegv@sdf.org>
+ * @author cyberta
*/
public class Gateway {
@@ -51,60 +61,69 @@ public class Gateway {
private JSONObject secrets;
private JSONObject gateway;
- private String mName;
+ private String name;
private int timezone;
- private VpnProfile mVpnProfile;
+ private int apiVersion;
+ private HashMap<Connection.TransportType, VpnProfile> vpnProfiles;
/**
* Build a gateway object from a JSON OpenVPN gateway definition in eip-service.json
* and create a VpnProfile belonging to it.
*/
- public Gateway(JSONObject eip_definition, JSONObject secrets, JSONObject gateway, Context context) {
+ public Gateway(JSONObject eipDefinition, JSONObject secrets, JSONObject gateway, Context context) {
this.gateway = gateway;
this.secrets = secrets;
- generalConfiguration = getGeneralConfiguration(eip_definition);
- timezone = getTimezone(eip_definition);
- mName = locationAsName(eip_definition);
-
- mVpnProfile = createVPNProfile();
- System.out.println("###########" + mName + "###########");
- mVpnProfile.mName = mName;
+ generalConfiguration = getGeneralConfiguration(eipDefinition);
+ timezone = getTimezone(eipDefinition);
+ name = locationAsName(eipDefinition);
+ apiVersion = getApiVersion(eipDefinition);
+ vpnProfiles = createVPNProfiles(context);
+ }
+ private void addProfileInfos(Context context, HashMap<Connection.TransportType, VpnProfile> profiles) {
Set<String> excludedAppsVpn = PreferenceHelper.getExcludedApps(context);
- if (excludedAppsVpn != null) {
- mVpnProfile.mAllowedAppsVpn = new HashSet<>(excludedAppsVpn);
- }
- else {
- mVpnProfile.mAllowedAppsVpn = null;
+ for (VpnProfile profile : profiles.values()) {
+ profile.mName = name;
+ profile.mGatewayIp = gateway.optString(IP_ADDRESS);
+ if (excludedAppsVpn != null) {
+ profile.mAllowedAppsVpn = new HashSet<>(excludedAppsVpn);
+ }
}
-
}
- private JSONObject getGeneralConfiguration(JSONObject eip_definition) {
+ private JSONObject getGeneralConfiguration(JSONObject eipDefinition) {
try {
- return eip_definition.getJSONObject("openvpn_configuration");
+ return eipDefinition.getJSONObject(OPENVPN_CONFIGURATION);
} catch (JSONException e) {
return new JSONObject();
}
}
- private int getTimezone(JSONObject eip_definition) {
- JSONObject location = getLocationInfo(eip_definition);
- return location.optInt("timezone");
+ private int getTimezone(JSONObject eipDefinition) {
+ JSONObject location = getLocationInfo(eipDefinition);
+ return location.optInt(TIMEZONE);
+ }
+
+ private int getApiVersion(JSONObject eipDefinition) {
+ return eipDefinition.optInt(VERSION);
+ }
+
+ public String getRemoteIP() {
+ return gateway.optString(IP_ADDRESS);
}
- private String locationAsName(JSONObject eip_definition) {
- JSONObject location = getLocationInfo(eip_definition);
- return location.optString("name");
+ private String locationAsName(JSONObject eipDefinition) {
+ JSONObject location = getLocationInfo(eipDefinition);
+ return location.optString(NAME);
}
private JSONObject getLocationInfo(JSONObject eipDefinition) {
try {
- JSONObject locations = eipDefinition.getJSONObject("locations");
+ JSONObject locations = eipDefinition.getJSONObject(LOCATIONS);
- return locations.getJSONObject(gateway.getString("location"));
+ return locations.getJSONObject(gateway.getString(LOCATION));
} catch (JSONException e) {
return new JSONObject();
}
@@ -113,32 +132,29 @@ public class Gateway {
/**
* Create and attach the VpnProfile to our gateway object
*/
- private VpnProfile createVPNProfile() {
+ private @NonNull HashMap<Connection.TransportType, VpnProfile> createVPNProfiles(Context context) {
+ HashMap<Connection.TransportType, VpnProfile> profiles = new HashMap<>();
try {
- ConfigParser cp = new ConfigParser();
-
- VpnConfigGenerator vpnConfigurationGenerator = new VpnConfigGenerator(generalConfiguration, secrets, gateway);
- String configuration = vpnConfigurationGenerator.generate();
-
- cp.parseConfig(new StringReader(configuration));
- return cp.convertProfile();
- } catch (ConfigParser.ConfigParseError e) {
- // FIXME We didn't get a VpnProfile! Error handling! and log level
- e.printStackTrace();
- return null;
- } catch (IOException e) {
+ VpnConfigGenerator vpnConfigurationGenerator = new VpnConfigGenerator(generalConfiguration, secrets, gateway, apiVersion);
+ profiles = vpnConfigurationGenerator.generateVpnProfiles();
+ addProfileInfos(context, profiles);
+ } catch (ConfigParser.ConfigParseError | IOException | JSONException e) {
// FIXME We didn't get a VpnProfile! Error handling! and log level
e.printStackTrace();
- return null;
}
+ return profiles;
}
public String getName() {
- return mName;
+ return name;
}
- public VpnProfile getProfile() {
- return mVpnProfile;
+ public HashMap<Connection.TransportType, VpnProfile> getProfiles() {
+ return vpnProfiles;
+ }
+
+ public VpnProfile getProfile(Connection.TransportType transportType) {
+ return vpnProfiles.get(transportType);
}
public int getTimezone() {
@@ -150,17 +166,4 @@ public class Gateway {
return new Gson().toJson(this, Gateway.class);
}
- @Override
- public boolean equals(Object obj) {
- return obj instanceof Gateway &&
- (this.mVpnProfile != null &&
- ((Gateway) obj).mVpnProfile != null &&
- this.mVpnProfile.mConnections != null &&
- ((Gateway) obj).mVpnProfile != null &&
- this.mVpnProfile.mConnections.length > 0 &&
- ((Gateway) obj).mVpnProfile.mConnections.length > 0 &&
- this.mVpnProfile.mConnections[0].mServerName != null &&
- this.mVpnProfile.mConnections[0].mServerName.equals(((Gateway) obj).mVpnProfile.mConnections[0].mServerName)) ||
- this.mVpnProfile == null && ((Gateway) obj).mVpnProfile == null;
- }
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java
index 2bd666bf..0ba0f207 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaySelector.java
@@ -36,7 +36,7 @@ public class GatewaySelector {
}
}
- Log.e(TAG, "There are less than " + nClosest + " Gateways available.");
+ Log.e(TAG, "There are less than " + (nClosest + 1) + " Gateways available.");
return null;
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java
index 6bd7b4a3..0847a07e 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java
@@ -28,11 +28,12 @@ import org.json.JSONObject;
import java.lang.reflect.Type;
import java.util.ArrayList;
-import java.util.List;
+import java.util.LinkedHashMap;
import se.leap.bitmaskclient.Provider;
import se.leap.bitmaskclient.utils.PreferenceHelper;
+import static se.leap.bitmaskclient.Constants.GATEWAYS;
import static se.leap.bitmaskclient.Constants.PROVIDER_PRIVATE_KEY;
import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE;
@@ -45,11 +46,10 @@ public class GatewaysManager {
private Context context;
private SharedPreferences preferences;
- private List<Gateway> gateways = new ArrayList<>();
+ private LinkedHashMap<String, Gateway> gateways = new LinkedHashMap<>();
private Type listType = new TypeToken<ArrayList<Gateway>>() {}.getType();
- GatewaysManager(Context context, SharedPreferences preferences) {
- this.context = context;
+ GatewaysManager(SharedPreferences preferences) {
this.preferences = preferences;
}
@@ -58,7 +58,7 @@ public class GatewaysManager {
* @return the n closest Gateway
*/
public Gateway select(int nClosest) {
- GatewaySelector gatewaySelector = new GatewaySelector(gateways);
+ GatewaySelector gatewaySelector = new GatewaySelector(new ArrayList<>(gateways.values()));
return gatewaySelector.select(nClosest);
}
@@ -88,37 +88,21 @@ public class GatewaysManager {
*/
void fromEipServiceJson(JSONObject eipDefinition) {
try {
- JSONArray gatewaysDefined = eipDefinition.getJSONArray("gateways");
+ JSONArray gatewaysDefined = eipDefinition.getJSONArray(GATEWAYS);
for (int i = 0; i < gatewaysDefined.length(); i++) {
JSONObject gw = gatewaysDefined.getJSONObject(i);
- if (isOpenVpnGateway(gw)) {
- JSONObject secrets = secretsConfiguration();
- Gateway aux = new Gateway(eipDefinition, secrets, gw, this.context);
- if (!gateways.contains(aux)) {
- addGateway(aux);
- }
+ JSONObject secrets = secretsConfiguration();
+ Gateway aux = new Gateway(eipDefinition, secrets, gw, this.context);
+ if (gateways.get(aux.getRemoteIP()) == null) {
+ addGateway(aux);
}
}
- } catch (JSONException e) {
+ } catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
- /**
- * check if a gateway is an OpenVpn gateway
- * @param gateway to check
- * @return true if gateway is an OpenVpn gateway otherwise false
- */
- private boolean isOpenVpnGateway(JSONObject gateway) {
- try {
- String transport = gateway.getJSONObject("capabilities").getJSONArray("transport").toString();
- return transport.contains("openvpn");
- } catch (JSONException e) {
- return false;
- }
- }
-
private JSONObject secretsConfiguration() {
JSONObject result = new JSONObject();
try {
@@ -137,7 +121,7 @@ public class GatewaysManager {
}
private void addGateway(Gateway gateway) {
- gateways.add(gateway);
+ gateways.put(gateway.getRemoteIP(), gateway);
}
/**
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java b/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java
index 6f0ccf18..d9bf5dd3 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java
@@ -20,48 +20,125 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.HashMap;
import java.util.Iterator;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.ConfigParser;
+import de.blinkt.openvpn.core.connection.Connection;
import se.leap.bitmaskclient.Provider;
+import se.leap.bitmaskclient.pluggableTransports.Obfs4Options;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN;
+import static se.leap.bitmaskclient.Constants.CAPABILITIES;
+import static se.leap.bitmaskclient.Constants.IP_ADDRESS;
+import static se.leap.bitmaskclient.Constants.OPTIONS;
+import static se.leap.bitmaskclient.Constants.PORTS;
+import static se.leap.bitmaskclient.Constants.PROTOCOLS;
import static se.leap.bitmaskclient.Constants.PROVIDER_PRIVATE_KEY;
import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.Constants.REMOTE;
+import static se.leap.bitmaskclient.Constants.TRANSPORT;
+import static se.leap.bitmaskclient.Constants.TYPE;
+import static se.leap.bitmaskclient.pluggableTransports.Dispatcher.DISPATCHER_IP;
+import static se.leap.bitmaskclient.pluggableTransports.Dispatcher.DISPATCHER_PORT;
public class VpnConfigGenerator {
-
- private JSONObject general_configuration;
+ private JSONObject generalConfiguration;
private JSONObject gateway;
private JSONObject secrets;
+ private JSONObject obfs4Transport;
+ private int apiVersion;
+
public final static String TAG = VpnConfigGenerator.class.getSimpleName();
private final String newLine = System.getProperty("line.separator"); // Platform new line
- public VpnConfigGenerator(JSONObject general_configuration, JSONObject secrets, JSONObject gateway) {
- this.general_configuration = general_configuration;
+ public VpnConfigGenerator(JSONObject generalConfiguration, JSONObject secrets, JSONObject gateway, int apiVersion) throws ConfigParser.ConfigParseError {
+ this.generalConfiguration = generalConfiguration;
this.gateway = gateway;
this.secrets = secrets;
+ this.apiVersion = apiVersion;
+ checkCapabilities();
}
- public String generate() {
- return
- generalConfiguration()
- + newLine
- + gatewayConfiguration()
- + newLine
- + secretsConfiguration()
- + newLine
- + androidCustomizations();
+ public void checkCapabilities() throws ConfigParser.ConfigParseError {
+
+ try {
+ if (apiVersion == 3) {
+ JSONArray supportedTransports = gateway.getJSONObject(CAPABILITIES).getJSONArray(TRANSPORT);
+ for (int i = 0; i < supportedTransports.length(); i++) {
+ JSONObject transport = supportedTransports.getJSONObject(i);
+ if (transport.getString(TYPE).equals(OBFS4.toString())) {
+ obfs4Transport = transport;
+ break;
+ }
+ }
+ }
+
+ } catch (JSONException e) {
+ throw new ConfigParser.ConfigParseError("Api version ("+ apiVersion +") did not match required JSON fields");
+ }
+ }
+
+ public HashMap<Connection.TransportType, VpnProfile> generateVpnProfiles() throws
+ ConfigParser.ConfigParseError,
+ NumberFormatException,
+ JSONException,
+ IOException {
+ HashMap<Connection.TransportType, VpnProfile> profiles = new HashMap<>();
+ profiles.put(OPENVPN, createProfile(OPENVPN));
+ if (supportsObfs4()) {
+ profiles.put(OBFS4, createProfile(OBFS4));
+ }
+ return profiles;
+ }
+
+ private boolean supportsObfs4(){
+ return obfs4Transport != null;
+ }
+
+ private String getConfigurationString(Connection.TransportType transportType) {
+ return generalConfiguration()
+ + newLine
+ + gatewayConfiguration(transportType)
+ + newLine
+ + androidCustomizations()
+ + newLine
+ + secretsConfiguration();
+ }
+
+ private VpnProfile createProfile(Connection.TransportType transportType) throws IOException, ConfigParser.ConfigParseError, JSONException {
+ String configuration = getConfigurationString(transportType);
+ ConfigParser icsOpenvpnConfigParser = new ConfigParser();
+ icsOpenvpnConfigParser.parseConfig(new StringReader(configuration));
+ if (transportType == OBFS4) {
+ icsOpenvpnConfigParser.setObfs4Options(getObfs4Options());
+ }
+ return icsOpenvpnConfigParser.convertProfile(transportType);
+ }
+
+ private Obfs4Options getObfs4Options() throws JSONException {
+ JSONObject transportOptions = obfs4Transport.getJSONObject(OPTIONS);
+ String iatMode = transportOptions.getString("iat-mode");
+ String cert = transportOptions.getString("cert");
+ String port = obfs4Transport.getJSONArray(PORTS).getString(0);
+ String ip = gateway.getString(IP_ADDRESS);
+ return new Obfs4Options(ip, port, cert, iatMode);
}
private String generalConfiguration() {
String commonOptions = "";
try {
- Iterator keys = general_configuration.keys();
+ Iterator keys = generalConfiguration.keys();
while (keys.hasNext()) {
String key = keys.next().toString();
commonOptions += key + " ";
- for (String word : String.valueOf(general_configuration.get(key)).split(" "))
+ for (String word : String.valueOf(generalConfiguration.get(key)).split(" "))
commonOptions += word + " ";
commonOptions += newLine;
@@ -76,41 +153,95 @@ public class VpnConfigGenerator {
return commonOptions;
}
- private String gatewayConfiguration() {
+ private String gatewayConfiguration(Connection.TransportType transportType) {
String remotes = "";
- String ipAddressKeyword = "ip_address";
- String remoteKeyword = "remote";
- String portsKeyword = "ports";
- String protocolKeyword = "protocols";
- String capabilitiesKeyword = "capabilities";
-
+ StringBuilder stringBuilder = new StringBuilder();
try {
- String ip_address = gateway.getString(ipAddressKeyword);
- JSONObject capabilities = gateway.getJSONObject(capabilitiesKeyword);
- JSONArray ports = capabilities.getJSONArray(portsKeyword);
- for (int i = 0; i < ports.length(); i++) {
- String port_specific_remotes = "";
- int port = ports.getInt(i);
- JSONArray protocols = capabilities.getJSONArray(protocolKeyword);
- for (int j = 0; j < protocols.length(); j++) {
- String protocol = protocols.optString(j);
- String new_remote = remoteKeyword + " " + ip_address + " " + port + " " + protocol + newLine;
-
- port_specific_remotes += new_remote;
- }
- remotes += port_specific_remotes;
+ String ipAddress = gateway.getString(IP_ADDRESS);
+ JSONObject capabilities = gateway.getJSONObject(CAPABILITIES);
+ switch (apiVersion) {
+ default:
+ case 1:
+ case 2:
+ gatewayConfigApiv1(stringBuilder, ipAddress, capabilities);
+ break;
+ case 3:
+ JSONArray transports = capabilities.getJSONArray(TRANSPORT);
+ gatewayConfigApiv3(transportType, stringBuilder, ipAddress, transports);
+ break;
}
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
+
+ remotes = stringBuilder.toString();
if (remotes.endsWith(newLine)) {
remotes = remotes.substring(0, remotes.lastIndexOf(newLine));
}
return remotes;
}
+ private void gatewayConfigApiv3(Connection.TransportType transportType, StringBuilder stringBuilder, String ipAddress, JSONArray transports) throws JSONException {
+ if (transportType == OBFS4) {
+ obfs4GatewayConfigApiv3(stringBuilder, ipAddress, transports);
+ } else {
+ ovpnGatewayConfigApi3(stringBuilder, ipAddress, transports);
+ }
+ }
+
+ private void gatewayConfigApiv1(StringBuilder stringBuilder, String ipAddress, JSONObject capabilities) throws JSONException {
+ int port;
+ String protocol;
+ JSONArray ports = capabilities.getJSONArray(PORTS);
+ for (int i = 0; i < ports.length(); i++) {
+ port = ports.getInt(i);
+ JSONArray protocols = capabilities.getJSONArray(PROTOCOLS);
+ for (int j = 0; j < protocols.length(); j++) {
+ protocol = protocols.optString(j);
+ String newRemote = REMOTE + " " + ipAddress + " " + port + " " + protocol + newLine;
+ stringBuilder.append(newRemote);
+ }
+ }
+ }
+
+ private void ovpnGatewayConfigApi3(StringBuilder stringBuilder, String ipAddress, JSONArray transports) throws JSONException {
+ String port;
+ String protocol;
+ JSONObject openvpnTransport = getTransport(transports, OPENVPN);
+ JSONArray ports = openvpnTransport.getJSONArray(PORTS);
+ for (int j = 0; j < ports.length(); j++) {
+ port = ports.getString(j);
+ JSONArray protocols = openvpnTransport.getJSONArray(PROTOCOLS);
+ for (int k = 0; k < protocols.length(); k++) {
+ protocol = protocols.optString(k);
+ String newRemote = REMOTE + " " + ipAddress + " " + port + " " + protocol + newLine;
+ stringBuilder.append(newRemote);
+ }
+ }
+ }
+
+ private JSONObject getTransport(JSONArray transports, Connection.TransportType transportType) throws JSONException {
+ JSONObject selectedTransport = new JSONObject();
+ for (int i = 0; i < transports.length(); i++) {
+ JSONObject transport = transports.getJSONObject(i);
+ if (transport.getString(TYPE).equals(transportType.toString())) {
+ selectedTransport = transport;
+ break;
+ }
+ }
+ return selectedTransport;
+ }
+
+ private void obfs4GatewayConfigApiv3(StringBuilder stringBuilder, String ipAddress, JSONArray transports) throws JSONException {
+ JSONObject obfs4Transport = getTransport(transports, OBFS4);
+ String route = "route " + ipAddress + " 255.255.255.255 net_gateway" + newLine;
+ stringBuilder.append(route);
+ String remote = REMOTE + " " + DISPATCHER_IP + " " + DISPATCHER_PORT + " " + obfs4Transport.getJSONArray(PROTOCOLS).getString(0) + newLine;
+ stringBuilder.append(remote);
+ }
+
private String secretsConfiguration() {
try {
String ca =
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/BinaryInstaller.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/BinaryInstaller.java
new file mode 100644
index 00000000..0d6aa61e
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/BinaryInstaller.java
@@ -0,0 +1,204 @@
+/* Copyright (c) 2009, Nathan Freitas, Orbot / The Guardian Project - http://openideals.com/guardian */
+/* See LICENSE for licensing information */
+
+package se.leap.bitmaskclient.pluggableTransports;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.TimeoutException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+public class BinaryInstaller {
+
+ File installFolder;
+ Context context;
+
+ public BinaryInstaller(Context context, File installFolder)
+ {
+ this.installFolder = installFolder;
+
+ this.context = context;
+ }
+
+ public void deleteDirectory(File file) {
+ if( file.exists() ) {
+ if (file.isDirectory()) {
+ File[] files = file.listFiles();
+ for(int i=0; i<files.length; i++) {
+ if(files[i].isDirectory()) {
+ deleteDirectory(files[i]);
+ }
+ else {
+ files[i].delete();
+ }
+ }
+ }
+
+ file.delete();
+ }
+ }
+
+ private final static String COMMAND_RM_FORCE = "rm -f ";
+ private final static String MP3_EXT = ".mp3";
+ //
+ /*
+ * Extract the resources from the APK file using ZIP
+ */
+ public File installResource (String basePath, String assetKey, boolean overwrite) throws IOException, FileNotFoundException, TimeoutException
+ {
+
+ InputStream is;
+ File outFile;
+
+ outFile = new File(installFolder, assetKey);
+
+ if (outFile.exists() && (!overwrite)) {
+ Log.d("BINARY_INSTALLER", "Binary already exists! Using " + outFile.getCanonicalPath());
+ return outFile;
+ }
+
+ deleteDirectory(installFolder);
+ installFolder.mkdirs();
+
+ Log.d("BINARY_INSTALLER", "Search asset in " + basePath + "/" + assetKey);
+
+ is = context.getAssets().open(basePath + '/' + assetKey);
+ streamToFile(is,outFile, false, false);
+ setExecutable(outFile);
+
+ Log.d("BINARY_INSTALLER", "Asset copied from " + basePath + "/" + assetKey + " to: " + outFile.getCanonicalPath());
+
+ return outFile;
+ }
+
+
+ private final static int FILE_WRITE_BUFFER_SIZE = 1024*8;
+ /*
+ * Write the inputstream contents to the file
+ */
+ public static boolean streamToFile(InputStream stm, File outFile, boolean append, boolean zip) throws IOException
+
+ {
+ byte[] buffer = new byte[FILE_WRITE_BUFFER_SIZE];
+
+ int bytecount;
+
+ OutputStream stmOut = new FileOutputStream(outFile.getAbsolutePath(), append);
+ ZipInputStream zis = null;
+
+ if (zip)
+ {
+ zis = new ZipInputStream(stm);
+ ZipEntry ze = zis.getNextEntry();
+ stm = zis;
+
+ }
+
+ while ((bytecount = stm.read(buffer)) > 0)
+ {
+
+ stmOut.write(buffer, 0, bytecount);
+
+ }
+
+ stmOut.close();
+ stm.close();
+
+ if (zis != null)
+ zis.close();
+
+
+ return true;
+
+ }
+
+ //copy the file from inputstream to File output - alternative impl
+ public static boolean copyFile (InputStream is, File outputFile)
+ {
+
+ try {
+ if (outputFile.exists())
+ outputFile.delete();
+
+ boolean newFile = outputFile.createNewFile();
+ DataOutputStream out = new DataOutputStream(new FileOutputStream(outputFile));
+ DataInputStream in = new DataInputStream(is);
+
+ int b = -1;
+ byte[] data = new byte[1024];
+
+ while ((b = in.read(data)) != -1) {
+ out.write(data);
+ }
+
+ if (b == -1); //rejoice
+
+ //
+ out.flush();
+ out.close();
+ in.close();
+ // chmod?
+
+ return newFile;
+
+
+ } catch (IOException ex) {
+ Log.e("Binaryinstaller", "error copying binary", ex);
+ return false;
+ }
+
+ }
+
+ /**
+ * Copies a raw resource file, given its ID to the given location
+ * @param ctx context
+ * @param resid resource id
+ * @param file destination file
+ * @param mode file permissions (E.g.: "755")
+ * @throws IOException on error
+ * @throws InterruptedException when interrupted
+ */
+ public static void copyRawFile(Context ctx, int resid, File file, String mode, boolean isZipd) throws IOException, InterruptedException
+ {
+ final String abspath = file.getAbsolutePath();
+ // Write the iptables binary
+ final FileOutputStream out = new FileOutputStream(file);
+ InputStream is = ctx.getResources().openRawResource(resid);
+
+ if (isZipd)
+ {
+ ZipInputStream zis = new ZipInputStream(is);
+ ZipEntry ze = zis.getNextEntry();
+ is = zis;
+ }
+
+ byte buf[] = new byte[1024];
+ int len;
+ while ((len = is.read(buf)) > 0) {
+ out.write(buf, 0, len);
+ }
+ out.close();
+ is.close();
+ // Change the permissions
+ Runtime.getRuntime().exec("chmod "+mode+" "+abspath).waitFor();
+ }
+
+
+ private void setExecutable(File fileBin) {
+ fileBin.setReadable(true);
+ fileBin.setExecutable(true);
+ fileBin.setWritable(false);
+ fileBin.setWritable(true, true);
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Dispatcher.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Dispatcher.java
new file mode 100644
index 00000000..8e787b57
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Dispatcher.java
@@ -0,0 +1,216 @@
+/**
+ * Copyright (c) 2019 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.pluggableTransports;
+
+import android.content.Context;
+import android.support.annotation.WorkerThread;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.util.StringTokenizer;
+
+
+/**
+ * Created by cyberta on 22.02.19.
+ */
+
+public class Dispatcher {
+ private static final String ASSET_KEY = "piedispatcher";
+ public static final String DISPATCHER_PORT = "4430";
+ public static final String DISPATCHER_IP = "127.0.0.1";
+ private static final String TAG = Dispatcher.class.getName();
+ private final String remoteIP;
+ private final String remotePort;
+ private final String certificate;
+ private final String iatMode;
+ private File fileDispatcher;
+ private Context context;
+ private Thread dispatcherThread = null;
+ private int dispatcherPid = -1;
+
+ public Dispatcher(Context context, Obfs4Options obfs4Options) {
+ this.context = context.getApplicationContext();
+ this.remoteIP = obfs4Options.remoteIP;
+ this.remotePort = obfs4Options.remotePort;
+ this.certificate = obfs4Options.cert;
+ this.iatMode = obfs4Options.iatMode;
+ }
+
+ @WorkerThread
+ public void initSync() {
+ try {
+ fileDispatcher = installDispatcher();
+
+ // start dispatcher
+ dispatcherThread = new Thread(() -> {
+ try {
+ StringBuilder dispatcherLog = new StringBuilder();
+ String dispatcherCommand = fileDispatcher.getCanonicalPath() +
+ " -transparent" +
+ " -client" +
+ " -state " + context.getFilesDir().getCanonicalPath() + "/state" +
+ " -target " + remoteIP + ":" + remotePort +
+ " -transports obfs4" +
+ " -options \"" + String.format("{\\\"cert\\\": \\\"%s\\\", \\\"iatMode\\\": \\\"%s\\\"}\"", certificate, iatMode) +
+ " -logLevel DEBUG -enableLogging" +
+ " -proxylistenaddr "+ DISPATCHER_IP + ":" + DISPATCHER_PORT;
+
+ Log.d(TAG, "dispatcher command: " + dispatcherCommand);
+ runBlockingCmd(new String[]{dispatcherCommand}, dispatcherLog);
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+ dispatcherThread.start();
+
+ // get pid of dispatcher, try several times in case the dispatcher
+ // process is not spawned yet
+ StringBuilder log = new StringBuilder();
+ String pidCommand = "ps | grep piedispatcher";
+ for (int i = 0; i < 5; i++) {
+ runBlockingCmd(new String[]{pidCommand}, log);
+ if (!TextUtils.isEmpty(log)) {
+ break;
+ }
+ Thread.sleep(100);
+ }
+
+ String output = log.toString();
+ StringTokenizer st = new StringTokenizer(output, " ");
+ st.nextToken(); // proc owner
+ dispatcherPid = Integer.parseInt(st.nextToken().trim());
+ } catch(Exception e){
+ if (dispatcherThread.isAlive()) {
+ Log.e(TAG, e.getMessage() + ". Shutting down Dispatcher thread.");
+ stop();
+ }
+ }
+ }
+
+ public String getPort() {
+ return DISPATCHER_PORT;
+ }
+
+ public void stop() {
+ Log.d(TAG, "Shutting down Dispatcher thread.");
+ if (dispatcherThread != null && dispatcherThread.isAlive()) {
+ try {
+ killProcess(dispatcherPid);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ dispatcherThread.interrupt();
+ }
+ }
+
+ private void killProcess(int pid) throws Exception {
+ String killPid = "kill -9 " + pid;
+ runCmd(new String[]{killPid}, null, false);
+ }
+
+ public boolean isRunning() {
+ return dispatcherThread != null && dispatcherThread.isAlive();
+ }
+
+ private File installDispatcher(){
+ File fileDispatcher = null;
+ BinaryInstaller bi = new BinaryInstaller(context,context.getFilesDir());
+
+ String arch = System.getProperty("os.arch");
+ if (arch.contains("arm"))
+ arch = "armeabi-v7a";
+ else
+ arch = "x86";
+
+ try {
+ fileDispatcher = bi.installResource(arch, ASSET_KEY, false);
+ } catch (Exception ioe) {
+ Log.d(TAG,"Couldn't install dispatcher: " + ioe);
+ }
+
+ return fileDispatcher;
+ }
+
+ @WorkerThread
+ private void runBlockingCmd(String[] cmds, StringBuilder log) throws Exception {
+ runCmd(cmds, log, true);
+ }
+
+ @WorkerThread
+ private int runCmd(String[] cmds, StringBuilder log,
+ boolean waitFor) throws Exception {
+
+ int exitCode = -1;
+ Process proc = Runtime.getRuntime().exec("sh");
+ OutputStreamWriter out = new OutputStreamWriter(proc.getOutputStream());
+
+ try {
+ for (String cmd : cmds) {
+ Log.d(TAG, "executing CMD: " + cmd);
+ out.write(cmd);
+ out.write("\n");
+ }
+
+ out.flush();
+ out.write("exit\n");
+ out.flush();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ out.close();
+ }
+
+ if (waitFor) {
+ // Consume the "stdout"
+ InputStreamReader reader = new InputStreamReader(proc.getInputStream());
+ readToLogString(reader, log);
+
+ // Consume the "stderr"
+ reader = new InputStreamReader(proc.getErrorStream());
+ readToLogString(reader, log);
+
+ try {
+ exitCode = proc.waitFor();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return exitCode;
+ }
+
+ private void readToLogString(InputStreamReader reader, StringBuilder log) throws IOException {
+ final char buf[] = new char[10];
+ int read = 0;
+ try {
+ while ((read = reader.read(buf)) != -1) {
+ if (log != null)
+ log.append(buf, 0, read);
+ }
+ } catch (IOException e) {
+ reader.close();
+ throw new IOException(e);
+ }
+ reader.close();
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Obfs4Options.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Obfs4Options.java
new file mode 100644
index 00000000..2f9cb732
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Obfs4Options.java
@@ -0,0 +1,18 @@
+package se.leap.bitmaskclient.pluggableTransports;
+
+import java.io.Serializable;
+
+public class Obfs4Options implements Serializable {
+ public String cert;
+ public String iatMode;
+ public String remoteIP;
+ public String remotePort;
+
+ public Obfs4Options(String remoteIP, String remotePort, String cert, String iatMode) {
+ this.cert = cert;
+ this.iatMode = iatMode;
+ this.remoteIP = remoteIP;
+ this.remotePort = remotePort;
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Shapeshifter.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Shapeshifter.java
new file mode 100644
index 00000000..175e236a
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Shapeshifter.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2019 LEAP Encryption Access Project and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient.pluggableTransports;
+
+import android.util.Log;
+
+import shapeshifter.ShapeShifter;
+
+public class Shapeshifter {
+
+ public static final String DISPATCHER_PORT = "4430";
+ public static final String DISPATCHER_IP = "127.0.0.1";
+ private static final String TAG = Shapeshifter.class.getSimpleName();
+
+ ShapeShifter shapeShifter;
+
+ public Shapeshifter(Obfs4Options options) {
+ shapeShifter = new ShapeShifter();
+ shapeShifter.setIatMode(Long.valueOf(options.iatMode));
+ shapeShifter.setSocksAddr(DISPATCHER_IP+":"+DISPATCHER_PORT);
+ shapeShifter.setTarget(options.remoteIP+":"+options.remotePort);
+ shapeShifter.setCert(options.cert);
+ Log.d(TAG, "shapeshifter initialized with: iat - " + shapeShifter.getIatMode() +
+ "; socksAddr - " + shapeShifter.getSocksAddr() +
+ "; target addr - " + shapeShifter.getTarget() +
+ "; cert - " + shapeShifter.getCert());
+ }
+
+ public boolean start() {
+ try {
+ shapeShifter.open();
+ Log.d(TAG, "shapeshifter opened");
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return false;
+ }
+
+ public boolean stop() {
+ try {
+ shapeShifter.close();
+ Log.d(TAG, "shapeshifter closed");
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return false;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java b/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java
index 9eb4c972..44b2a45d 100644
--- a/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/utils/PreferenceHelper.java
@@ -31,6 +31,7 @@ import static se.leap.bitmaskclient.Constants.PROVIDER_EIP_DEFINITION;
import static se.leap.bitmaskclient.Constants.PROVIDER_PRIVATE_KEY;
import static se.leap.bitmaskclient.Constants.PROVIDER_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.Constants.SHARED_PREFERENCES;
+import static se.leap.bitmaskclient.Constants.USE_PLUGGABLE_TRANSPORTS;
import static se.leap.bitmaskclient.Constants.EXCLUDED_APPS;
/**
@@ -213,6 +214,22 @@ public class PreferenceHelper {
apply();
}
+ public static boolean getUsePluggableTransports(Context context) {
+ if (context == null) {
+ return false;
+ }
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ return preferences.getBoolean(USE_PLUGGABLE_TRANSPORTS, false);
+ }
+
+ public static void usePluggableTransports(Context context, boolean isEnabled) {
+ if (context == null) {
+ return;
+ }
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ preferences.edit().putBoolean(USE_PLUGGABLE_TRANSPORTS, isEnabled).apply();
+ }
+
public static void saveBattery(Context context, boolean isEnabled) {
if (context == null) {
return;
@@ -284,5 +301,4 @@ public class PreferenceHelper {
preferences.edit().putString(key, value).apply();
}
-
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/views/IconSwitchEntry.java b/app/src/main/java/se/leap/bitmaskclient/views/IconSwitchEntry.java
new file mode 100644
index 00000000..02347b05
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/views/IconSwitchEntry.java
@@ -0,0 +1,104 @@
+package se.leap.bitmaskclient.views;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.support.v7.widget.SwitchCompat;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import se.leap.bitmaskclient.R;
+
+public class IconSwitchEntry extends LinearLayout {
+
+ private TextView textView;
+ private TextView subtitleView;
+ private ImageView iconView;
+ private SwitchCompat switchView;
+ private CompoundButton.OnCheckedChangeListener checkedChangeListener;
+
+ public IconSwitchEntry(Context context) {
+ super(context);
+ initLayout(context, null);
+ }
+
+ public IconSwitchEntry(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ initLayout(context, attrs);
+ }
+
+ public IconSwitchEntry(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initLayout(context, attrs);
+ }
+
+ @TargetApi(21)
+ public IconSwitchEntry(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initLayout(context, attrs);
+ }
+
+ void initLayout(Context context, AttributeSet attrs) {
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View rootview = inflater.inflate(R.layout.v_switch_list_item, this, true);
+ textView = rootview.findViewById(android.R.id.text1);
+ subtitleView = rootview.findViewById(R.id.subtitle);
+ iconView = rootview.findViewById(R.id.material_icon);
+ switchView = rootview.findViewById(R.id.option_switch);
+
+ if (attrs != null) {
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IconSwitchEntry);
+
+ String entryText = typedArray.getString(R.styleable.IconTextEntry_text);
+ if (entryText != null) {
+ textView.setText(entryText);
+ }
+
+ String subtitle = typedArray.getString(R.styleable.IconTextEntry_subtitle);
+ if (subtitle != null) {
+ subtitleView.setText(subtitle);
+ subtitleView.setVisibility(VISIBLE);
+ }
+
+ Drawable drawable = typedArray.getDrawable(R.styleable.IconTextEntry_icon);
+ if (drawable != null) {
+ iconView.setImageDrawable(drawable);
+ }
+
+ typedArray.recycle();
+ }
+ }
+
+ public void setOnCheckedChangeListener(CompoundButton.OnCheckedChangeListener listener) {
+ checkedChangeListener = listener;
+ switchView.setOnCheckedChangeListener(checkedChangeListener);
+ }
+
+ public void setText(@StringRes int id) {
+ textView.setText(id);
+ }
+
+ public void setIcon(@DrawableRes int id) {
+ iconView.setImageResource(id);
+ }
+
+ public void setChecked(boolean isChecked) {
+ switchView.setChecked(isChecked);
+ }
+
+ public void setCheckedQuietly(boolean isChecked) {
+ switchView.setOnCheckedChangeListener(null);
+ switchView.setChecked(isChecked);
+ switchView.setOnCheckedChangeListener(checkedChangeListener);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/views/IconTextEntry.java b/app/src/main/java/se/leap/bitmaskclient/views/IconTextEntry.java
new file mode 100644
index 00000000..0e86f506
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/views/IconTextEntry.java
@@ -0,0 +1,92 @@
+package se.leap.bitmaskclient.views;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import se.leap.bitmaskclient.R;
+
+
+public class IconTextEntry extends LinearLayout {
+
+ private TextView textView;
+ private ImageView iconView;
+ private TextView subtitleView;
+
+ public IconTextEntry(Context context) {
+ super(context);
+ initLayout(context, null);
+ }
+
+ public IconTextEntry(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ initLayout(context, attrs);
+ }
+
+ public IconTextEntry(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initLayout(context, attrs);
+ }
+
+ @TargetApi(21)
+ public IconTextEntry(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initLayout(context, attrs);
+ }
+
+ void initLayout(Context context, AttributeSet attrs) {
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View rootview = inflater.inflate(R.layout.v_icon_text_list_item, this, true);
+ textView = rootview.findViewById(android.R.id.text1);
+ subtitleView = rootview.findViewById(R.id.subtitle);
+ iconView = rootview.findViewById(R.id.material_icon);
+
+ if (attrs != null) {
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IconTextEntry);
+
+ String entryText = typedArray.getString(R.styleable.IconTextEntry_text);
+ if (entryText != null) {
+ textView.setText(entryText);
+ }
+
+ String subtitle = typedArray.getString(R.styleable.IconTextEntry_subtitle);
+ if (subtitle != null) {
+ subtitleView.setText(subtitle);
+ subtitleView.setVisibility(VISIBLE);
+ }
+
+ Drawable drawable = typedArray.getDrawable(R.styleable.IconTextEntry_icon);
+ if (drawable != null) {
+ iconView.setImageDrawable(drawable);
+ }
+
+ typedArray.recycle();
+ }
+
+
+ }
+
+ public void setText(@StringRes int id) {
+ textView.setText(id);
+ }
+
+ public void setText(CharSequence text) {
+ textView.setText(text);
+ }
+
+ public void setIcon(@DrawableRes int id) {
+ iconView.setImageResource(id);
+ }
+
+}
diff --git a/app/src/main/res/drawable-hdpi/ic_bridge_36.png b/app/src/main/res/drawable-hdpi/ic_bridge_36.png
new file mode 100644
index 00000000..e3acd2d1
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_bridge_36.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_bridge_36.png b/app/src/main/res/drawable-mdpi/ic_bridge_36.png
new file mode 100644
index 00000000..6c45a2d8
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_bridge_36.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_bridge_36.png b/app/src/main/res/drawable-xhdpi/ic_bridge_36.png
new file mode 100644
index 00000000..6f89408c
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_bridge_36.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_bridge_36.png b/app/src/main/res/drawable-xxhdpi/ic_bridge_36.png
new file mode 100644
index 00000000..d00613b8
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/ic_bridge_36.png
Binary files differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_bridge_36.png b/app/src/main/res/drawable-xxxhdpi/ic_bridge_36.png
new file mode 100644
index 00000000..8f531f5a
--- /dev/null
+++ b/app/src/main/res/drawable-xxxhdpi/ic_bridge_36.png
Binary files differ
diff --git a/app/src/main/res/layout-xlarge/v_icon_text_list_item.xml b/app/src/main/res/layout-xlarge/v_icon_text_list_item.xml
index 0192e080..798b47e3 100644
--- a/app/src/main/res/layout-xlarge/v_icon_text_list_item.xml
+++ b/app/src/main/res/layout-xlarge/v_icon_text_list_item.xml
@@ -1,6 +1,6 @@
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/item_container"
- android:layout_height="wrap_content"
+ android:layout_height="?android:attr/listPreferredItemHeight"
android:layout_width="match_parent"
android:orientation="horizontal"
xmlns:tools="http://schemas.android.com/tools">
@@ -27,6 +27,33 @@
android:paddingRight="?android:attr/listPreferredItemPaddingRight"
android:minHeight="?android:attr/listPreferredItemHeight"
tools:text="TEST"
+ android:layout_toEndOf="@id/material_icon"
+ android:layout_toRightOf="@+id/material_icon"
+ android:layout_above="@+id/subtitle"
/>
-</LinearLayout>
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:layout_alignParentBottom="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+ android:paddingBottom="8dp"
+ tools:text="TEST"
+ android:visibility="gone"
+ android:layout_toEndOf="@id/material_icon"
+ android:layout_toRightOf="@+id/material_icon"
+ />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1px"
+ android:background="@android:color/darker_gray"
+ android:layout_alignParentBottom="true"
+ />
+</RelativeLayout>
diff --git a/app/src/main/res/layout-xlarge/v_switch_list_item.xml b/app/src/main/res/layout-xlarge/v_switch_list_item.xml
index d692070e..3d81af11 100644
--- a/app/src/main/res/layout-xlarge/v_switch_list_item.xml
+++ b/app/src/main/res/layout-xlarge/v_switch_list_item.xml
@@ -29,6 +29,25 @@
tools:text="TEST"
android:layout_toEndOf="@id/material_icon"
android:layout_toRightOf="@+id/material_icon"
+ android:layout_above="@+id/subtitle"
+ />
+
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:layout_alignParentBottom="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+ android:paddingBottom="4dp"
+ tools:text="TEST"
+ android:visibility="gone"
+ android:layout_toEndOf="@id/material_icon"
+ android:layout_toRightOf="@+id/material_icon"
/>
<android.support.v7.widget.SwitchCompat
@@ -45,4 +64,10 @@
android:minHeight="?android:attr/listPreferredItemHeight"
android:checked="false"
tools:text="" />
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1px"
+ android:background="@android:color/darker_gray"
+ android:layout_alignParentBottom="true"
+ />
</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/f_drawer_main.xml b/app/src/main/res/layout/f_drawer_main.xml
index b04d7b87..f6c9b2bb 100644
--- a/app/src/main/res/layout/f_drawer_main.xml
+++ b/app/src/main/res/layout/f_drawer_main.xml
@@ -1,70 +1,134 @@
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical"
+ android:layout_width="match_parent"
android:background="@color/colorBackground"
tools:context="se.leap.bitmaskclient.drawer.NavigationDrawerFragment"
android:clickable="true"
- android:focusable="true">
+ android:focusable="true"
+ android:fillViewport="true"
+ >
- <FrameLayout
+ <LinearLayout
android:layout_width="match_parent"
- android:layout_height="150dp">
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ >
- <android.support.v7.widget.AppCompatImageView
- android:id="@+id/background"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:adjustViewBounds="false"
- android:cropToPadding="false"
- android:scaleType="fitXY"
- app:srcCompat="@drawable/background_drawer" />
-
- <android.support.v7.widget.AppCompatImageView
- android:id="@+id/foreground"
+ <FrameLayout
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:scaleType="centerInside"
- app:srcCompat="@drawable/drawer_logo" />
- </FrameLayout>
- <RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
+ android:layout_height="150dp">
- <ListView
- android:id="@+id/accountList"
- android:layout_width="match_parent"
+ <android.support.v7.widget.AppCompatImageView
+ android:id="@+id/background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:adjustViewBounds="false"
+ android:cropToPadding="false"
+ android:scaleType="fitXY"
+ app:srcCompat="@drawable/background_drawer" />
+
+ <android.support.v7.widget.AppCompatImageView
+ android:id="@+id/foreground"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="centerInside"
+ app:srcCompat="@drawable/drawer_logo" />
+ </FrameLayout>
+
+ <se.leap.bitmaskclient.views.IconTextEntry
+ android:id="@+id/account"
android:layout_height="wrap_content"
- android:isScrollContainer="false"
+ android:layout_width="wrap_content"
+ />
+
+ <se.leap.bitmaskclient.views.IconTextEntry
+ android:id="@+id/switch_provider"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ app:text="@string/switch_provider_menu_option"
+ app:icon="@drawable/ic_switch_provider_36"
+ android:visibility="gone"
/>
<View
- android:id="@+id/divider"
- android:layout_below="@id/accountList"
android:layout_width="match_parent"
- android:layout_height="1px"
- android:background="@android:color/darker_gray"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:minHeight="20dp"
+ android:background="@color/black800_high_transparent"
/>
- <FrameLayout
+ <se.leap.bitmaskclient.views.IconSwitchEntry
+ android:id="@+id/battery_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:text="@string/save_battery"
+ app:icon="@drawable/ic_battery_36"
+ />
+
+ <se.leap.bitmaskclient.views.IconSwitchEntry
+ android:id="@+id/bridges_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:text="@string/nav_drawer_obfuscated_connection"
+ app:subtitle="@string/nav_drawer_subtitle_obfuscated_connection"
+ app:icon="@drawable/ic_bridge_36"
+ android:visibility="gone"
+ />
+
+ <se.leap.bitmaskclient.views.IconTextEntry
+ android:id="@+id/always_on_vpn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:text="@string/always_on_vpn"
+ app:subtitle="@string/subtitle_always_on_vpn"
+ app:icon="@drawable/ic_always_on_36"
+ android:visibility="gone"
+ />
+
+ <se.leap.bitmaskclient.views.IconTextEntry
+ android:id="@+id/exclude_apps"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:text="@string/exclude_apps_fragment_title"
+ app:icon="@drawable/ic_shield_remove_grey600_36dp"
+ android:visibility="gone"
+ />
+
+ <View
android:layout_width="match_parent"
+ android:layout_height="20dp"
+ android:background="@color/black800_high_transparent"
+ />
+
+ <se.leap.bitmaskclient.views.IconTextEntry
+ android:id="@+id/donate"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_alignTop="@id/divider"
- android:layout_alignParentBottom="true"
- >
- <ListView
- android:id="@+id/settingsList"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom"
- />
- </FrameLayout>
+ app:text="@string/donate_title"
+ app:icon="@drawable/ic_donate_36"
+ android:visibility="gone"
+ />
+ <se.leap.bitmaskclient.views.IconTextEntry
+ android:id="@+id/log"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:text="@string/log_fragment_title"
+ app:icon="@drawable/ic_log_36"
+ />
+
+ <se.leap.bitmaskclient.views.IconTextEntry
+ android:id="@+id/about"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:text="@string/about_fragment_title"
+ app:icon="@drawable/ic_about_36"
+ />
- </RelativeLayout>
+ </LinearLayout>
-</LinearLayout> \ No newline at end of file
+</ScrollView>
diff --git a/app/src/main/res/layout/v_icon_text_list_item.xml b/app/src/main/res/layout/v_icon_text_list_item.xml
index 0631b2fc..64cc474a 100644
--- a/app/src/main/res/layout/v_icon_text_list_item.xml
+++ b/app/src/main/res/layout/v_icon_text_list_item.xml
@@ -1,6 +1,6 @@
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/item_container"
- android:layout_height="wrap_content"
+ android:layout_height="?android:attr/listPreferredItemHeightSmall"
android:layout_width="match_parent"
android:orientation="horizontal"
xmlns:tools="http://schemas.android.com/tools">
@@ -26,6 +26,33 @@
android:paddingRight="?android:attr/listPreferredItemPaddingRight"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
tools:text="TEST"
+ android:layout_toEndOf="@id/material_icon"
+ android:layout_toRightOf="@+id/material_icon"
+ android:layout_above="@+id/subtitle"
/>
-</LinearLayout>
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:layout_alignParentBottom="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+ android:paddingBottom="4dp"
+ tools:text="TEST"
+ android:visibility="gone"
+ android:layout_toEndOf="@id/material_icon"
+ android:layout_toRightOf="@+id/material_icon"
+ />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1px"
+ android:background="@android:color/darker_gray"
+ android:layout_alignParentBottom="true"
+ />
+</RelativeLayout>
diff --git a/app/src/main/res/layout/v_switch_list_item.xml b/app/src/main/res/layout/v_switch_list_item.xml
index 26060a73..967d7a97 100644
--- a/app/src/main/res/layout/v_switch_list_item.xml
+++ b/app/src/main/res/layout/v_switch_list_item.xml
@@ -29,6 +29,25 @@
tools:text="TEST"
android:layout_toEndOf="@id/material_icon"
android:layout_toRightOf="@+id/material_icon"
+ android:layout_above="@+id/subtitle"
+ />
+
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:layout_alignParentBottom="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+ android:paddingBottom="4dp"
+ tools:text="TEST"
+ android:visibility="gone"
+ android:layout_toEndOf="@id/material_icon"
+ android:layout_toRightOf="@+id/material_icon"
/>
<android.support.v7.widget.SwitchCompat
@@ -45,4 +64,11 @@
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:checked="false"
tools:text="" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1px"
+ android:background="@android:color/darker_gray"
+ android:layout_alignParentBottom="true"
+ />
</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index eb9626bc..d3a88b81 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -1,6 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
+ <!--TODO: check that it's not needed and throw it out!-->
<declare-styleable name="foo">
<attr name="textColorError" format="color" />
</declare-styleable>
+
+ <attr name="text" format="string|reference"/>
+ <attr name="icon" format="reference"/>
+ <attr name="subtitle" format="string|reference"/>
+ <declare-styleable name="IconSwitchEntry">
+ <attr name="text"/>
+ <attr name="subtitle" />
+ <attr name="icon"/>
+ </declare-styleable>
+
+ <declare-styleable name="IconTextEntry">
+ <attr name="text"/>
+ <attr name="subtitle" />
+ <attr name="icon"/>
+ </declare-styleable>
</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e685cff5..27f508d5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -103,6 +103,7 @@
<string name="save_battery">Save battery</string>
<string name="save_battery_message">Background data connections will hibernate when your phone is inactive.</string>
<string name="always_on_vpn">Always-on VPN</string>
+ <string name="subtitle_always_on_vpn">Open Android System Settings</string>
<string name="do_not_show_again">Do not show again</string>
<string name="always_on_vpn_user_message">To enable always-on VPN in Android VPN Settings click on the configure icon [img src] and turn the switch on.</string>
<string name="always_on_blocking_vpn_user_message">To protect your privacy optimally, you should also activate the option \"Block connections without VPN\".</string>
@@ -111,6 +112,10 @@
<string name="donate_message">LEAP depends on donations and grants. Please donate today if you value secure communication that is easy for both the end-user and the service provider.</string>
<string name="donate_button_remind_later">Remind me later</string>
<string name="donate_button_donate">Donate</string>
+ <string name="obfuscated_connection">Using an obfuscated connection.</string>
+ <string name="obfuscated_connection_try">Trying an obfuscated connection.</string>
+ <string name="nav_drawer_obfuscated_connection">Using Bridges</string>
+ <string name="nav_drawer_subtitle_obfuscated_connection">Circumvent VPN filtering</string>
<string name="warning_exclude_apps_message">Be careful of excluding apps from VPN. This will reveal your identity and compromise your security.</string>
</resources>
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 51a8ea0e..7e98ccf4 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -20,4 +20,8 @@
<item name="android:windowBackground">@drawable/splash_page</item>
</style>
+ <style name="invisibleTheme" parent="@android:style/Theme.Translucent.NoTitleBar">
+ <item name="android:windowAnimationStyle">@null</item>
+ </style>
+
</resources>
diff --git a/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java b/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java
index 6d9671b1..10582cf3 100644
--- a/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java
+++ b/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java
@@ -20,6 +20,7 @@ package se.leap.bitmaskclient;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Bundle;
+import android.support.multidex.BuildConfig;
import android.util.Pair;
import org.json.JSONException;
@@ -29,6 +30,7 @@ import java.io.IOException;
import java.net.URL;
import java.util.List;
+import de.blinkt.openvpn.core.VpnStatus;
import okhttp3.OkHttpClient;
import se.leap.bitmaskclient.eip.EIP;
import se.leap.bitmaskclient.utils.ConfigHelper;
@@ -134,8 +136,12 @@ public class ProviderApiManager extends ProviderApiManagerBase {
return result;
}
+ if (BuildConfig.DEBUG) {
+ VpnStatus.logDebug("PROVIDER JSON: " + providerDotJsonString);
+ }
try {
JSONObject providerJson = new JSONObject(providerDotJsonString);
+
if (provider.define(providerJson)) {
result.putBoolean(BROADCAST_RESULT_KEY, true);
} else {
@@ -143,9 +149,7 @@ public class ProviderApiManager extends ProviderApiManagerBase {
}
} catch (JSONException e) {
- String reason_to_fail = pickErrorMessage(providerDotJsonString);
- result.putString(ERRORS, reason_to_fail);
- result.putBoolean(BROADCAST_RESULT_KEY, false);
+ setErrorResult(result, providerDotJsonString);
}
return result;
}
@@ -163,14 +167,17 @@ public class ProviderApiManager extends ProviderApiManagerBase {
String eipServiceUrl = providerJson.getString(Provider.API_URL) + "/" + providerJson.getString(Provider.API_VERSION) + "/" + EIP.SERVICE_API_PATH;
eipServiceJsonString = downloadWithProviderCA(provider.getCaCert(), eipServiceUrl);
JSONObject eipServiceJson = new JSONObject(eipServiceJsonString);
-
- provider.setEipServiceJson(eipServiceJson);
-
- result.putBoolean(BROADCAST_RESULT_KEY, true);
+ if (BuildConfig.DEBUG) {
+ VpnStatus.logDebug("EIP SERVICE JSON: " + eipServiceJsonString);
+ }
+ if (eipServiceJson.has(ERRORS)) {
+ setErrorResult(result, eipServiceJsonString);
+ } else {
+ provider.setEipServiceJson(eipServiceJson);
+ result.putBoolean(BROADCAST_RESULT_KEY, true);
+ }
} catch (NullPointerException | JSONException e) {
- String reasonToFail = pickErrorMessage(eipServiceJsonString);
- result.putString(ERRORS, reasonToFail);
- result.putBoolean(BROADCAST_RESULT_KEY, false);
+ setErrorResult(result, eipServiceJsonString);
}
return result;
}
@@ -187,14 +194,15 @@ public class ProviderApiManager extends ProviderApiManagerBase {
URL newCertStringUrl = new URL(provider.getApiUrlWithVersion() + "/" + PROVIDER_VPN_CERTIFICATE);
String certString = downloadWithProviderCA(provider.getCaCert(), newCertStringUrl.toString());
+ if (BuildConfig.DEBUG) {
+ VpnStatus.logDebug("VPN CERT: " + certString);
+ }
if (ConfigHelper.checkErroneousDownload(certString)) {
if (certString == null || certString.isEmpty()) {
// probably 204
setErrorResult(result, error_io_exception_user_message, null);
} else {
- String reasonToFail = pickErrorMessage(certString);
- result.putString(ERRORS, reasonToFail);
- result.putBoolean(BROADCAST_RESULT_KEY, false);
+ setErrorResult(result, certString);
return result;
}
}
@@ -217,6 +225,9 @@ public class ProviderApiManager extends ProviderApiManagerBase {
if (validCertificate(provider, certString)) {
provider.setCaCert(certString);
preferences.edit().putString(Provider.CA_CERT + "." + providerDomain, certString).apply();
+ if (BuildConfig.DEBUG) {
+ VpnStatus.logDebug("CA CERT: " + certString);
+ }
result.putBoolean(BROADCAST_RESULT_KEY, true);
} else {
setErrorResult(result, warning_corrupted_provider_cert, ERROR_CERTIFICATE_PINNING.toString());
diff --git a/app/src/sharedTest/java/se.leap.bitmaskclient/testutils/TestSetupHelper.java b/app/src/sharedTest/java/se.leap.bitmaskclient/testutils/TestSetupHelper.java
index 8835b75d..669abc84 100644
--- a/app/src/sharedTest/java/se.leap.bitmaskclient/testutils/TestSetupHelper.java
+++ b/app/src/sharedTest/java/se.leap.bitmaskclient/testutils/TestSetupHelper.java
@@ -18,6 +18,7 @@
package se.leap.bitmaskclient.testutils;
import org.json.JSONException;
+import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
@@ -40,6 +41,9 @@ public class TestSetupHelper {
while (line != null) {
sb.append(line);
line = br.readLine();
+ if (line != null) {
+ sb.append("\n");
+ }
}
return sb.toString();
@@ -47,25 +51,32 @@ public class TestSetupHelper {
public static Provider getConfiguredProvider() throws IOException, JSONException {
- return getProvider(null, null, null);
+ return getProvider(null, null, null, null);
}
- public static Provider getProvider(String domain, String caCertFile, String jsonFile) {
+ public static Provider getProvider(String domain, String caCertFile, String providerJson, String eipServiceJson) {
if (domain == null)
domain = "https://riseup.net";
if (caCertFile == null)
caCertFile = "riseup.net.pem";
- if (jsonFile == null)
- jsonFile = "riseup.net.json";
+ if (providerJson == null)
+ providerJson = "riseup.net.json";
+ if (eipServiceJson == null) {
+ eipServiceJson = "riseup.service.json";
+ }
try {
- return new Provider(
+ Provider p = new Provider(
new URL(domain),
getInputAsString(TestSetupHelper.class.getClassLoader().getResourceAsStream(caCertFile)),
- getInputAsString(TestSetupHelper.class.getClassLoader().getResourceAsStream(jsonFile))
+ getInputAsString(TestSetupHelper.class.getClassLoader().getResourceAsStream(providerJson))
);
- } catch (IOException e) {
+ JSONObject eipServiceJsonObject = new JSONObject(
+ getInputAsString(TestSetupHelper.class.getClassLoader().getResourceAsStream(eipServiceJson)));
+ p.setEipServiceJson(eipServiceJsonObject);
+ return p;
+ } catch (IOException | JSONException e) {
e.printStackTrace();
}
return null;
diff --git a/app/src/test/java/se/leap/bitmaskclient/ProviderTest.java b/app/src/test/java/se/leap/bitmaskclient/ProviderTest.java
index a141edec..c4b8f65e 100644
--- a/app/src/test/java/se/leap/bitmaskclient/ProviderTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/ProviderTest.java
@@ -46,4 +46,25 @@ public class ProviderTest {
assertTrue(defaultProviders.contains(p2));
assertFalse(defaultProviders.contains(p3));
}
+
+ @Test
+ public void testIsPluggableTransportsSupported_Obfs4_returnsTrue() throws Exception {
+ Provider p1 = TestSetupHelper.getProvider(
+ "https://pt.demo.bitmask.net",
+ null,
+ null,
+ "ptdemo.bitmask.eip-service.json");
+ assertTrue(p1.supportsPluggableTransports());
+ }
+
+ @Test
+ public void testIsPluggableTransportsSupported_noObfs4_returnsFalse() throws Exception {
+ Provider p1 = TestSetupHelper.getProvider(
+ null,
+ null,
+ null,
+ "eip-service-two-gateways.json");
+ assertFalse(p1.supportsPluggableTransports());
+ }
+
}
diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/EipStatusTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/EipStatusTest.java
index 8495f962..8ba7f5fc 100644
--- a/app/src/test/java/se/leap/bitmaskclient/eip/EipStatusTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/eip/EipStatusTest.java
@@ -10,6 +10,7 @@ import org.powermock.modules.junit4.PowerMockRunner;
import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.ConnectionStatus;
import de.blinkt.openvpn.core.VpnStatus;
+import de.blinkt.openvpn.core.connection.Connection;
import se.leap.bitmaskclient.R;
import se.leap.bitmaskclient.testutils.MockHelper;
import se.leap.bitmaskclient.testutils.TestSetupHelper;
@@ -23,6 +24,7 @@ import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_START;
import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_VPNPAUSED;
import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT;
import static de.blinkt.openvpn.core.ConnectionStatus.UNKNOWN_LEVEL;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN;
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.powermock.api.mockito.PowerMockito.doNothing;
@@ -61,7 +63,7 @@ public class EipStatusTest {
public void testUpdateState_LEVEL_VPNPAUSED_hasPersistentTun() throws Exception {
mockStatic(PreferenceHelper.class);
- VpnProfile mockVpnProfile = new VpnProfile("mockProfile");
+ VpnProfile mockVpnProfile = new VpnProfile("mockProfile", OPENVPN);
mockVpnProfile.mPersistTun = true;
doNothing().when(PreferenceHelper.class);
VpnStatus.setLastConnectedVpnProfile(null, mockVpnProfile);
@@ -74,7 +76,7 @@ public class EipStatusTest {
public void testUpdateState_LEVEL_VPNPAUSED_hasNotPersistentTun() throws Exception {
mockStatic(PreferenceHelper.class);
- VpnProfile mockVpnProfile = new VpnProfile("mockProfile");
+ VpnProfile mockVpnProfile = new VpnProfile("mockProfile", OPENVPN);
mockVpnProfile.mPersistTun = false;
doNothing().when(PreferenceHelper.class);
VpnStatus.setLastConnectedVpnProfile(null, mockVpnProfile);
diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
index 160e5ddd..e4c5f390 100644
--- a/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
@@ -48,7 +48,7 @@ public class GatewaysManagerTest {
when(sharedPreferences.getString(eq(Constants.PROVIDER_VPN_CERTIFICATE), anyString())).thenReturn(secrets.getString(Constants.PROVIDER_VPN_CERTIFICATE));
- gatewaysManager = new GatewaysManager(mockContext, sharedPreferences);
+ gatewaysManager = new GatewaysManager(sharedPreferences);
}
@Test
@@ -57,6 +57,15 @@ public class GatewaysManagerTest {
assertEquals(0, gatewaysManager.size());
}
+ @Test
+ public void testFromEipServiceJson_ignoreDuplicateGateways_apiv3() throws Exception {
+ String eipServiceJson = TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_three_mixed_gateways.json"));
+ gatewaysManager.fromEipServiceJson(new JSONObject(eipServiceJson));
+ assertEquals(3, gatewaysManager.size());
+ eipServiceJson = TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo.bitmask.eip-service.json"));
+ gatewaysManager.fromEipServiceJson(new JSONObject(eipServiceJson));
+ assertEquals(3, gatewaysManager.size());
+ }
@Test
public void testFromEipServiceJson_ignoreDuplicateGateways() throws Exception {
diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java
index 3070fc0b..21781abb 100644
--- a/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java
@@ -52,6 +52,7 @@ import static se.leap.bitmaskclient.Constants.PROVIDER_KEY;
import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_NOK;
import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_OK;
+import static se.leap.bitmaskclient.testutils.BackendMockResponses.BackendMockProvider.TestBackendErrorCase.ERROR_CASE_FETCH_EIP_SERVICE_CERTIFICATE_INVALID;
import static se.leap.bitmaskclient.testutils.BackendMockResponses.BackendMockProvider.TestBackendErrorCase.ERROR_CASE_MICONFIGURED_PROVIDER;
import static se.leap.bitmaskclient.testutils.BackendMockResponses.BackendMockProvider.TestBackendErrorCase.ERROR_CASE_UPDATED_CERTIFICATE;
import static se.leap.bitmaskclient.testutils.BackendMockResponses.BackendMockProvider.TestBackendErrorCase.NO_ERROR;
@@ -267,7 +268,7 @@ public class ProviderApiManagerTest {
@Test
public void test_handleIntentSetupProvider_preseededProviderAndCA_outdatedCertificate() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, JSONException {
- Provider provider = getProvider(null ,"outdated_cert.pem", null);
+ Provider provider = getProvider(null ,"outdated_cert.pem", null, null);
mockProviderApiConnector(NO_ERROR);
providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
@@ -384,7 +385,7 @@ public class ProviderApiManagerTest {
@Test
public void test_handleIntentSetupProvider_outdatedPreseededProviderAndCA_successfulConfiguration() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, JSONException {
- Provider provider = getProvider(null, null, "riseup_net_outdated_config.json");
+ Provider provider = getProvider(null, null, "riseup_net_outdated_config.json", null);
mockFingerprintForCertificate(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
mockProviderApiConnector(NO_ERROR);
@@ -403,4 +404,26 @@ public class ProviderApiManagerTest {
providerApiManager.handleIntent(providerApiCommand);
}
+
+ @Test
+ public void test_handleIntentSetupProvider_failingEipServiceFetch_failedConfiguration() throws IOException, NoSuchAlgorithmException, CertificateEncodingException {
+ Provider provider = new Provider("https://riseup.net");
+
+ mockFingerprintForCertificate("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+ mockProviderApiConnector(ERROR_CASE_FETCH_EIP_SERVICE_CERTIFICATE_INVALID);
+ providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+
+ Bundle expectedResult = mockBundle();
+ expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
+ expectedResult.putParcelable(PROVIDER_KEY, provider);
+ expectedResult.putString(ERRORS, "This is not a trusted Bitmask provider.");
+
+ Intent providerApiCommand = mockIntent();
+
+ providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
+ providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
+ providerApiCommand.putExtra(PROVIDER_KEY, provider);
+
+ providerApiManager.handleIntent(providerApiCommand);
+ }
}
diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java
index 8c8cdb61..40da8e43 100644
--- a/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java
@@ -1,31 +1,191 @@
package se.leap.bitmaskclient.eip;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.text.TextUtils;
+import android.util.Log;
+
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.io.File;
+import java.util.HashMap;
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.connection.Connection;
+import se.leap.bitmaskclient.testutils.MockHelper;
import se.leap.bitmaskclient.testutils.TestSetupHelper;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN;
+import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.powermock.api.mockito.PowerMockito.mockStatic;
+import static org.powermock.api.mockito.PowerMockito.when;
+import static se.leap.bitmaskclient.testutils.MockHelper.mockTextUtils;
/**
* Created by cyberta on 03.10.17.
*/
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({Log.class, TextUtils.class, PreferenceManager.class})
public class VpnConfigGeneratorTest {
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ Context context;
+
private VpnConfigGenerator vpnConfigGenerator;
private JSONObject generalConfig;
private JSONObject gateway;
private JSONObject secrets;
- String expectedVPNConfig_tcp_udp = "cipher AES-128-CBC \n" +
- "auth SHA1 \n" +
- "tun-ipv6 true \n" +
+ String expectedVPNConfig_v1_tcp_udp = "# Config for OpenVPN 2.x\n" +
+ "# Enables connection to GUI\n" +
+ "management /data/data/se.leap.bitmask/mgmtsocket unix\n" +
+ "management-client\n" +
+ "management-query-passwords\n" +
+ "management-hold\n" +
+ "\n" +
+ "setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
+ "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+ "machine-readable-output\n" +
+ "allow-recursive-routing\n" +
+ "ifconfig-nowarn\n" +
+ "client\n" +
+ "verb 4\n" +
+ "connect-retry 2 300\n" +
+ "resolv-retry 60\n" +
+ "dev tun\n" +
+ "remote 198.252.153.84 443 tcp-client\n" +
+ "remote 198.252.153.84 443 udp\n" +
+ "<ca>\n" +
+ "-----BEGIN CERTIFICATE-----\n" +
+ "MIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt\n" +
+ "YXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v\n" +
+ "Yml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw\n" +
+ "FgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV\n" +
+ "BAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw\n" +
+ "ggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai\n" +
+ "dHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB\n" +
+ "7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r/bEHUSevmR09BRp86syHZerdNGpXYhcQ84\n" +
+ "CA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+\n" +
+ "znCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe/RGk4\n" +
+ "MEqGFuOzrtsgEhPIX0hplhb0Tgz/rtug+yTT7oJjBa3u20AAOQ38/M99EfdeJvc4\n" +
+ "lPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu/qt3DfvLfhboq+0\n" +
+ "bQvLUPXrVDr70onv5UDjpmEA/cLmaIqqrduuTkFZOym65/PfAPvpGnt7crQj/Ibl\n" +
+ "DEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB\n" +
+ "lfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy\n" +
+ "YMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw\n" +
+ "XjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH/BAQDAgIE\n" +
+ "MAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w\n" +
+ "DQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl\n" +
+ "cXYyB6hY5hv/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY\n" +
+ "k/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj\n" +
+ "RnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG\n" +
+ "htD/JKwt6xBmNwktH0GI/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX\n" +
+ "EIQ0ZR56bFuJA/CwValBqV/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J\n" +
+ "aF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l\n" +
+ "mlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK\n" +
+ "G6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co\n" +
+ "Ja8zlx64jmMZPg/t3wWqkZgXZ14qnbyG5/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d\n" +
+ "69db12/g4f6phldhxiWuGC/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e\n" +
+ "yV8e\n" +
+ "-----END CERTIFICATE-----\n" +
+ "\n" +
+ "</ca>\n" +
+ "<key>\n" +
+ "-----BEGIN RSA PRIVATE KEY-----\n" +
+ "MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDUTYWeGgsHS+fjijmziniNqw6h\n" +
+ "MBpyK4S/cM6PxV28C33VuOWPTMcIYesctjZANWFCggfFTQSjV5Qaxq9UK4i27tayLbCdlVS6hpbl\n" +
+ "Vf4DuI3Gj1Pv1rtITBShtvCf3T7yBnjW4wVpOpsUAAOViKUSvUU3kPPMFWhiGQw8yHYr82ts6XMo\n" +
+ "jwMoonW5Ml4e7C7Cr22QesC63q7emNcpUd0pZGT9C33RgDAHZDMrlyjo4HEp1JbUfB0gbmXElJbE\n" +
+ "1TNdZ62HhgmMjzTUN1GGrQ1t91AEoEQwaK65o4YSj+yFv6KXZZz5OWaz94tKiN9v26EXtBFmRlyb\n" +
+ "6+D9ynSd9LghAgMBAAECggEBANPHLRXkhsHVj1EkzqBx7gXr8CEMmiTvknFh9zvltrZhhDoRQjWr\n" +
+ "chPDkcRHY2Cznvy4N0YyqQDD2ULIlZdSAgPxxothFoBruWSD47yMBmLx08ORsDpcqt/YvPAATJI8\n" +
+ "IpFNsXcyaXBp/M57oRemgnxp/8UJPJmFdWX99H4hvffh/jdj7POgYiWUaAl37XTYZKZ4nzKU2wpL\n" +
+ "EDLj9RKPz9gG7CYp2zrLC9LaAsrXVrKwPBw6g+XwbClaqFj97db3mrY4lr6mTo89qmus1AU+fBDH\n" +
+ "3Xlpmc8JwB+30TvhRNKrpLx9cEjuEj7K1gm8Y4dWCjPi+lNbtAyUBcgPJFa/81ECgYEA7pLoBU/Y\n" +
+ "ZYjyHFca8FvDBcBh6haHfqJr9doXWtgjDrbi3o2n5wHqfKhFWOH6vPEQozkOVeX1ze6HOiRmGBpW\n" +
+ "r+r7x8TD25L7I6HJw3M351RWOAfkF0w/RTVdetcTgduQtfN1u6BDhYSVceXMjyQYx7MhfETWI8Gh\n" +
+ "KSYm8OEDYiUCgYEA489fmbrCcUnXzpTsbswJ5NmSoEXbcX8cLxnQuzE0z9GHhQdrMjOpXR76reTW\n" +
+ "6jcuudarNcwRUYSWWhjCDKHhpx4HhasWPaHgr7jIzcRw8yZSJRSxKr8sl1qh6g7s47JcmfXOMWLt\n" +
+ "yuyE933XrT19Th4ODZHY40Uv35mPjMi9d00CgYEAyRNAQtndBRa7GG/B4Ls2T+6pl+aNJIo4e+no\n" +
+ "rURlp800wWabEPRocdBRQmyULBLxduBr2LIMzhgwGSz8b2wji/l9ZA3PFY135bxClVzSzUIjuO3N\n" +
+ "rGUzHl2wAAyuAFDSUshzfkPBJRNt8aVBF5PQ3t93ZYmPAmv8LPZe875yX5ECgYEAsUEcwK/ZNW7g\n" +
+ "dQPZR4iJNkC4Xu6cBZ6Cnn92swBheEYvLSoNlX0vDZ7aLE3/jzQqrjzC8NP8sbH5jtbuvgeDXZX3\n" +
+ "AmGRp5j6C6A61ihAPmEVz3ZfN8SSfJ3vl//PAIg6lyz0J+cy4Q7RkwSeuVQ72Hl4M8TEvmmKC3Af\n" +
+ "ispy6Y0CgYEAgl1o2lo+ACyk+oVQPaaPqK3d7WOBFp4eR2nXFor/vsx9igQOlZUgzRDQsR8jo1o9\n" +
+ "efOSBf87igrZGgssys89pWa2dnXnz5PMmzkKr6bw4D9Ez6u6Puc9UZhGw/8wDYg6fSosdB9utspm\n" +
+ "M698ycef7jBNMDgmhpSvfw5GctoNQ4s=\n" +
+ "-----END RSA PRIVATE KEY-----\n" +
+ "</key>\n" +
+ "<cert>\n" +
+ "-----BEGIN CERTIFICATE-----\n" +
+ "MIIEjDCCAnSgAwIBAgIQG6MBp/cd9DlY+7cdvp3R3jANBgkqhkiG9w0BAQsFADBmMRAwDgYDVQQK\n" +
+ "DAdCaXRtYXNrMRwwGgYDVQQLDBNodHRwczovL2JpdG1hc2submV0MTQwMgYDVQQDDCtCaXRtYXNr\n" +
+ "IFJvb3QgQ0EgKGNsaWVudCBjZXJ0aWZpY2F0ZXMgb25seSEpMB4XDTE0MTIwNTAwMDAwMFoXDTE1\n" +
+ "MDMwNTAwMDAwMFowLTErMCkGA1UEAwwiVU5MSU1JVEVEZDBwZDdkMzE4eTNtOHNkeXllaTFqYmZl\n" +
+ "eDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANRNhZ4aCwdL5+OKObOKeI2rDqEwGnIr\n" +
+ "hL9wzo/FXbwLfdW45Y9Mxwhh6xy2NkA1YUKCB8VNBKNXlBrGr1QriLbu1rItsJ2VVLqGluVV/gO4\n" +
+ "jcaPU+/Wu0hMFKG28J/dPvIGeNbjBWk6mxQAA5WIpRK9RTeQ88wVaGIZDDzIdivza2zpcyiPAyii\n" +
+ "dbkyXh7sLsKvbZB6wLrert6Y1ylR3SlkZP0LfdGAMAdkMyuXKOjgcSnUltR8HSBuZcSUlsTVM11n\n" +
+ "rYeGCYyPNNQ3UYatDW33UASgRDBorrmjhhKP7IW/opdlnPk5ZrP3i0qI32/boRe0EWZGXJvr4P3K\n" +
+ "dJ30uCECAwEAAaNvMG0wHQYDVR0OBBYEFK8bMVAM4GBB5sHptoIOAaIvlYueMAsGA1UdDwQEAwIH\n" +
+ "gDATBgNVHSUEDDAKBggrBgEFBQcDAjAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFId+E7bsWFsUWah9\n" +
+ "vZuPvZ7O+aJsMA0GCSqGSIb3DQEBCwUAA4ICAQAQOX81csVhvP422NKkZH7+g3npBpl+sEHedaGR\n" +
+ "xYPOu4HrA4TVF9h44sljRoRJyenGNdBZCXcLKHg889eePTf8Z5K3lTojp6hvwyA6tgxOMHT1kESW\n" +
+ "PfqnRw8mHfHJuE3g+4YNUMwggzwc/VZATdV/7M33sarVN9AUOHou9n9BizgCC+UnYlS+F2POumE3\n" +
+ "FbOhKo5uubI02MwBYlN2JVO2TBt1Q20w8wc6cU07Xi5Epp+1mkgFiOShkNtPcJmEyBWJhxDtSDOW\n" +
+ "2doqWYNqH2kq7B5R/kyyfcpFJqAnBTV7xs+C5rTS1mW7LpxfdCUMbYuLCpyxpO3A/DhAm8n47tUH\n" +
+ "lBtmo8Avdb8VdFpYiGBpB0o9kTFcsWFb2GkWFBduGfSEB8jUI7QtqhgZqocAKK/cweSRV8FwyUcn\n" +
+ "R0prRm3QEi9fbXqEddzjSY9y/lqWYzT7u+IOAQpKroeZ4wzgYperDNOUFuYk1rP7yuvjP2pV5rcN\n" +
+ "yPoBP60TPVWMRM4WJm6nTogAz2qBrFsf/XwT/ajzbsjT6HNB7QbRE+wkFkqspoXG5Agp7KQ8lW3L\n" +
+ "SKCDGOQJz7VIE85pD0tg7QEXBEw8oaRZtMjQ0Gvs25mxXAKka4wGasaWfYH6d0E+iKYcWn86V1rH\n" +
+ "K2ZoknT+Nno5jgjFuUR3fZseNizEfx7BteooKQ==\n" +
+ "-----END CERTIFICATE-----\n" +
+ "</cert>\n" +
+ "crl-verify file missing in config profile\n" +
+ "remote-cert-tls server\n" +
+ "cipher AES-128-CBC\n" +
+ "auth SHA1\n" +
+ "persist-tun\n" +
+ "# persist-tun also enables pre resolving to avoid DNS resolve problem\n" +
+ "preresolve\n" +
+ "# Custom configuration options\n" +
+ "# You are on your on own here :)\n" +
+ "# These options found in the config file do not map to config settings:\n" +
"keepalive 10 30 \n" +
- "tls-cipher DHE-RSA-AES128-SHA \n" +
+ "tls-cipher DHE-RSA-AES128-SHA";
+
+ String expectedVPNConfig_v1_udp_tcp = "# Config for OpenVPN 2.x\n" +
+ "# Enables connection to GUI\n" +
+ "management /data/data/se.leap.bitmask/mgmtsocket unix\n" +
+ "management-client\n" +
+ "management-query-passwords\n" +
+ "management-hold\n" +
+ "\n" +
+ "setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
+ "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+ "machine-readable-output\n" +
+ "allow-recursive-routing\n" +
+ "ifconfig-nowarn\n" +
"client\n" +
- "remote 198.252.153.84 443 tcp\n" +
+ "verb 4\n" +
+ "connect-retry 2 300\n" +
+ "resolv-retry 60\n" +
+ "dev tun\n" +
"remote 198.252.153.84 443 udp\n" +
+ "remote 198.252.153.84 443 tcp-client\n" +
"<ca>\n" +
"-----BEGIN CERTIFICATE-----\n" +
"MIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt\n" +
@@ -112,18 +272,156 @@ public class VpnConfigGeneratorTest {
"K2ZoknT+Nno5jgjFuUR3fZseNizEfx7BteooKQ==\n" +
"-----END CERTIFICATE-----\n" +
"</cert>\n" +
+ "crl-verify file missing in config profile\n" +
"remote-cert-tls server\n" +
+ "cipher AES-128-CBC\n" +
+ "auth SHA1\n" +
"persist-tun\n" +
- "auth-retry nointeract";
+ "# persist-tun also enables pre resolving to avoid DNS resolve problem\n" +
+ "preresolve\n" +
+ "# Custom configuration options\n" +
+ "# You are on your on own here :)\n" +
+ "# These options found in the config file do not map to config settings:\n" +
+ "keepalive 10 30 \n" +
+ "tls-cipher DHE-RSA-AES128-SHA";
- String expectedVPNConfig_udp_tcp = "cipher AES-128-CBC \n" +
- "auth SHA1 \n" +
- "tun-ipv6 true \n" +
+ String expectedVPNConfig_v3_obfs4 = "# Config for OpenVPN 2.x\n" +
+ "# Enables connection to GUI\n" +
+ "management /data/data/se.leap.bitmask/mgmtsocket unix\n" +
+ "management-client\n" +
+ "management-query-passwords\n" +
+ "management-hold\n" +
+ "\n" +
+ "setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
+ "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+ "machine-readable-output\n" +
+ "allow-recursive-routing\n" +
+ "ifconfig-nowarn\n" +
+ "client\n" +
+ "verb 4\n" +
+ "connect-retry 2 300\n" +
+ "resolv-retry 60\n" +
+ "dev tun\n" +
+ "remote 127.0.0.1 4430 tcp-client\n" +
+ "<ca>\n" +
+ "-----BEGIN CERTIFICATE-----\n" +
+ "MIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt\n" +
+ "YXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v\n" +
+ "Yml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw\n" +
+ "FgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV\n" +
+ "BAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw\n" +
+ "ggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai\n" +
+ "dHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB\n" +
+ "7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r/bEHUSevmR09BRp86syHZerdNGpXYhcQ84\n" +
+ "CA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+\n" +
+ "znCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe/RGk4\n" +
+ "MEqGFuOzrtsgEhPIX0hplhb0Tgz/rtug+yTT7oJjBa3u20AAOQ38/M99EfdeJvc4\n" +
+ "lPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu/qt3DfvLfhboq+0\n" +
+ "bQvLUPXrVDr70onv5UDjpmEA/cLmaIqqrduuTkFZOym65/PfAPvpGnt7crQj/Ibl\n" +
+ "DEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB\n" +
+ "lfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy\n" +
+ "YMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw\n" +
+ "XjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH/BAQDAgIE\n" +
+ "MAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w\n" +
+ "DQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl\n" +
+ "cXYyB6hY5hv/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY\n" +
+ "k/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj\n" +
+ "RnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG\n" +
+ "htD/JKwt6xBmNwktH0GI/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX\n" +
+ "EIQ0ZR56bFuJA/CwValBqV/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J\n" +
+ "aF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l\n" +
+ "mlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK\n" +
+ "G6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co\n" +
+ "Ja8zlx64jmMZPg/t3wWqkZgXZ14qnbyG5/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d\n" +
+ "69db12/g4f6phldhxiWuGC/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e\n" +
+ "yV8e\n" +
+ "-----END CERTIFICATE-----\n" +
+ "\n" +
+ "</ca>\n" +
+ "<key>\n" +
+ "-----BEGIN RSA PRIVATE KEY-----\n" +
+ "MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDUTYWeGgsHS+fjijmziniNqw6h\n" +
+ "MBpyK4S/cM6PxV28C33VuOWPTMcIYesctjZANWFCggfFTQSjV5Qaxq9UK4i27tayLbCdlVS6hpbl\n" +
+ "Vf4DuI3Gj1Pv1rtITBShtvCf3T7yBnjW4wVpOpsUAAOViKUSvUU3kPPMFWhiGQw8yHYr82ts6XMo\n" +
+ "jwMoonW5Ml4e7C7Cr22QesC63q7emNcpUd0pZGT9C33RgDAHZDMrlyjo4HEp1JbUfB0gbmXElJbE\n" +
+ "1TNdZ62HhgmMjzTUN1GGrQ1t91AEoEQwaK65o4YSj+yFv6KXZZz5OWaz94tKiN9v26EXtBFmRlyb\n" +
+ "6+D9ynSd9LghAgMBAAECggEBANPHLRXkhsHVj1EkzqBx7gXr8CEMmiTvknFh9zvltrZhhDoRQjWr\n" +
+ "chPDkcRHY2Cznvy4N0YyqQDD2ULIlZdSAgPxxothFoBruWSD47yMBmLx08ORsDpcqt/YvPAATJI8\n" +
+ "IpFNsXcyaXBp/M57oRemgnxp/8UJPJmFdWX99H4hvffh/jdj7POgYiWUaAl37XTYZKZ4nzKU2wpL\n" +
+ "EDLj9RKPz9gG7CYp2zrLC9LaAsrXVrKwPBw6g+XwbClaqFj97db3mrY4lr6mTo89qmus1AU+fBDH\n" +
+ "3Xlpmc8JwB+30TvhRNKrpLx9cEjuEj7K1gm8Y4dWCjPi+lNbtAyUBcgPJFa/81ECgYEA7pLoBU/Y\n" +
+ "ZYjyHFca8FvDBcBh6haHfqJr9doXWtgjDrbi3o2n5wHqfKhFWOH6vPEQozkOVeX1ze6HOiRmGBpW\n" +
+ "r+r7x8TD25L7I6HJw3M351RWOAfkF0w/RTVdetcTgduQtfN1u6BDhYSVceXMjyQYx7MhfETWI8Gh\n" +
+ "KSYm8OEDYiUCgYEA489fmbrCcUnXzpTsbswJ5NmSoEXbcX8cLxnQuzE0z9GHhQdrMjOpXR76reTW\n" +
+ "6jcuudarNcwRUYSWWhjCDKHhpx4HhasWPaHgr7jIzcRw8yZSJRSxKr8sl1qh6g7s47JcmfXOMWLt\n" +
+ "yuyE933XrT19Th4ODZHY40Uv35mPjMi9d00CgYEAyRNAQtndBRa7GG/B4Ls2T+6pl+aNJIo4e+no\n" +
+ "rURlp800wWabEPRocdBRQmyULBLxduBr2LIMzhgwGSz8b2wji/l9ZA3PFY135bxClVzSzUIjuO3N\n" +
+ "rGUzHl2wAAyuAFDSUshzfkPBJRNt8aVBF5PQ3t93ZYmPAmv8LPZe875yX5ECgYEAsUEcwK/ZNW7g\n" +
+ "dQPZR4iJNkC4Xu6cBZ6Cnn92swBheEYvLSoNlX0vDZ7aLE3/jzQqrjzC8NP8sbH5jtbuvgeDXZX3\n" +
+ "AmGRp5j6C6A61ihAPmEVz3ZfN8SSfJ3vl//PAIg6lyz0J+cy4Q7RkwSeuVQ72Hl4M8TEvmmKC3Af\n" +
+ "ispy6Y0CgYEAgl1o2lo+ACyk+oVQPaaPqK3d7WOBFp4eR2nXFor/vsx9igQOlZUgzRDQsR8jo1o9\n" +
+ "efOSBf87igrZGgssys89pWa2dnXnz5PMmzkKr6bw4D9Ez6u6Puc9UZhGw/8wDYg6fSosdB9utspm\n" +
+ "M698ycef7jBNMDgmhpSvfw5GctoNQ4s=\n" +
+ "-----END RSA PRIVATE KEY-----\n" +
+ "</key>\n" +
+ "<cert>\n" +
+ "-----BEGIN CERTIFICATE-----\n" +
+ "MIIEjDCCAnSgAwIBAgIQG6MBp/cd9DlY+7cdvp3R3jANBgkqhkiG9w0BAQsFADBmMRAwDgYDVQQK\n" +
+ "DAdCaXRtYXNrMRwwGgYDVQQLDBNodHRwczovL2JpdG1hc2submV0MTQwMgYDVQQDDCtCaXRtYXNr\n" +
+ "IFJvb3QgQ0EgKGNsaWVudCBjZXJ0aWZpY2F0ZXMgb25seSEpMB4XDTE0MTIwNTAwMDAwMFoXDTE1\n" +
+ "MDMwNTAwMDAwMFowLTErMCkGA1UEAwwiVU5MSU1JVEVEZDBwZDdkMzE4eTNtOHNkeXllaTFqYmZl\n" +
+ "eDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANRNhZ4aCwdL5+OKObOKeI2rDqEwGnIr\n" +
+ "hL9wzo/FXbwLfdW45Y9Mxwhh6xy2NkA1YUKCB8VNBKNXlBrGr1QriLbu1rItsJ2VVLqGluVV/gO4\n" +
+ "jcaPU+/Wu0hMFKG28J/dPvIGeNbjBWk6mxQAA5WIpRK9RTeQ88wVaGIZDDzIdivza2zpcyiPAyii\n" +
+ "dbkyXh7sLsKvbZB6wLrert6Y1ylR3SlkZP0LfdGAMAdkMyuXKOjgcSnUltR8HSBuZcSUlsTVM11n\n" +
+ "rYeGCYyPNNQ3UYatDW33UASgRDBorrmjhhKP7IW/opdlnPk5ZrP3i0qI32/boRe0EWZGXJvr4P3K\n" +
+ "dJ30uCECAwEAAaNvMG0wHQYDVR0OBBYEFK8bMVAM4GBB5sHptoIOAaIvlYueMAsGA1UdDwQEAwIH\n" +
+ "gDATBgNVHSUEDDAKBggrBgEFBQcDAjAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFId+E7bsWFsUWah9\n" +
+ "vZuPvZ7O+aJsMA0GCSqGSIb3DQEBCwUAA4ICAQAQOX81csVhvP422NKkZH7+g3npBpl+sEHedaGR\n" +
+ "xYPOu4HrA4TVF9h44sljRoRJyenGNdBZCXcLKHg889eePTf8Z5K3lTojp6hvwyA6tgxOMHT1kESW\n" +
+ "PfqnRw8mHfHJuE3g+4YNUMwggzwc/VZATdV/7M33sarVN9AUOHou9n9BizgCC+UnYlS+F2POumE3\n" +
+ "FbOhKo5uubI02MwBYlN2JVO2TBt1Q20w8wc6cU07Xi5Epp+1mkgFiOShkNtPcJmEyBWJhxDtSDOW\n" +
+ "2doqWYNqH2kq7B5R/kyyfcpFJqAnBTV7xs+C5rTS1mW7LpxfdCUMbYuLCpyxpO3A/DhAm8n47tUH\n" +
+ "lBtmo8Avdb8VdFpYiGBpB0o9kTFcsWFb2GkWFBduGfSEB8jUI7QtqhgZqocAKK/cweSRV8FwyUcn\n" +
+ "R0prRm3QEi9fbXqEddzjSY9y/lqWYzT7u+IOAQpKroeZ4wzgYperDNOUFuYk1rP7yuvjP2pV5rcN\n" +
+ "yPoBP60TPVWMRM4WJm6nTogAz2qBrFsf/XwT/ajzbsjT6HNB7QbRE+wkFkqspoXG5Agp7KQ8lW3L\n" +
+ "SKCDGOQJz7VIE85pD0tg7QEXBEw8oaRZtMjQ0Gvs25mxXAKka4wGasaWfYH6d0E+iKYcWn86V1rH\n" +
+ "K2ZoknT+Nno5jgjFuUR3fZseNizEfx7BteooKQ==\n" +
+ "-----END CERTIFICATE-----\n" +
+ "</cert>\n" +
+ "crl-verify file missing in config profile\n" +
+ "route 37.218.247.60 255.255.255.255 net_gateway\n"+
+ "remote-cert-tls server\n" +
+ "cipher AES-128-CBC\n" +
+ "auth SHA1\n" +
+ "persist-tun\n" +
+ "# persist-tun also enables pre resolving to avoid DNS resolve problem\n" +
+ "preresolve\n" +
+ "# Custom configuration options\n" +
+ "# You are on your on own here :)\n" +
+ "# These options found in the config file do not map to config settings:\n" +
"keepalive 10 30 \n" +
- "tls-cipher DHE-RSA-AES128-SHA \n" +
+ "tls-cipher DHE-RSA-AES128-SHA \n";
+
+ String expectedVPNConfig_v3_ovpn_tcp_udp = "# Config for OpenVPN 2.x\n" +
+ "# Enables connection to GUI\n" +
+ "management /data/data/se.leap.bitmask/mgmtsocket unix\n" +
+ "management-client\n" +
+ "management-query-passwords\n" +
+ "management-hold\n" +
+ "\n" +
+ "setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
+ "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+ "machine-readable-output\n" +
+ "allow-recursive-routing\n" +
+ "ifconfig-nowarn\n" +
"client\n" +
- "remote 198.252.153.84 443 udp\n" +
- "remote 198.252.153.84 443 tcp\n" +
+ "verb 4\n" +
+ "connect-retry 2 300\n" +
+ "resolv-retry 60\n" +
+ "dev tun\n" +
+ "remote 37.218.247.60 1195 tcp-client\n" +
+ "remote 37.218.247.60 1195 udp\n" +
"<ca>\n" +
"-----BEGIN CERTIFICATE-----\n" +
"MIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt\n" +
@@ -210,33 +508,219 @@ public class VpnConfigGeneratorTest {
"K2ZoknT+Nno5jgjFuUR3fZseNizEfx7BteooKQ==\n" +
"-----END CERTIFICATE-----\n" +
"</cert>\n" +
+ "crl-verify file missing in config profile\n" +
"remote-cert-tls server\n" +
+ "cipher AES-128-CBC\n" +
+ "auth SHA1\n" +
"persist-tun\n" +
- "auth-retry nointeract";
+ "# persist-tun also enables pre resolving to avoid DNS resolve problem\n" +
+ "preresolve\n" +
+ "# Custom configuration options\n" +
+ "# You are on your on own here :)\n" +
+ "# These options found in the config file do not map to config settings:\n" +
+ "keepalive 10 30 \n" +
+ "tls-cipher DHE-RSA-AES128-SHA \n";
+
+ String expectedVPNConfig_v3_ovpn_udp_tcp = "# Config for OpenVPN 2.x\n" +
+ "# Enables connection to GUI\n" +
+ "management /data/data/se.leap.bitmask/mgmtsocket unix\n" +
+ "management-client\n" +
+ "management-query-passwords\n" +
+ "management-hold\n" +
+ "\n" +
+ "setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
+ "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+ "machine-readable-output\n" +
+ "allow-recursive-routing\n" +
+ "ifconfig-nowarn\n" +
+ "client\n" +
+ "verb 4\n" +
+ "connect-retry 2 300\n" +
+ "resolv-retry 60\n" +
+ "dev tun\n" +
+ "remote 37.218.247.60 1195 udp\n" +
+ "remote 37.218.247.60 1195 tcp-client\n" +
+ "<ca>\n" +
+ "-----BEGIN CERTIFICATE-----\n" +
+ "MIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt\n" +
+ "YXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v\n" +
+ "Yml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw\n" +
+ "FgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV\n" +
+ "BAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw\n" +
+ "ggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai\n" +
+ "dHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB\n" +
+ "7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r/bEHUSevmR09BRp86syHZerdNGpXYhcQ84\n" +
+ "CA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+\n" +
+ "znCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe/RGk4\n" +
+ "MEqGFuOzrtsgEhPIX0hplhb0Tgz/rtug+yTT7oJjBa3u20AAOQ38/M99EfdeJvc4\n" +
+ "lPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu/qt3DfvLfhboq+0\n" +
+ "bQvLUPXrVDr70onv5UDjpmEA/cLmaIqqrduuTkFZOym65/PfAPvpGnt7crQj/Ibl\n" +
+ "DEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB\n" +
+ "lfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy\n" +
+ "YMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw\n" +
+ "XjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH/BAQDAgIE\n" +
+ "MAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w\n" +
+ "DQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl\n" +
+ "cXYyB6hY5hv/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY\n" +
+ "k/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj\n" +
+ "RnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG\n" +
+ "htD/JKwt6xBmNwktH0GI/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX\n" +
+ "EIQ0ZR56bFuJA/CwValBqV/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J\n" +
+ "aF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l\n" +
+ "mlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK\n" +
+ "G6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co\n" +
+ "Ja8zlx64jmMZPg/t3wWqkZgXZ14qnbyG5/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d\n" +
+ "69db12/g4f6phldhxiWuGC/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e\n" +
+ "yV8e\n" +
+ "-----END CERTIFICATE-----\n" +
+ "\n" +
+ "</ca>\n" +
+ "<key>\n" +
+ "-----BEGIN RSA PRIVATE KEY-----\n" +
+ "MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDUTYWeGgsHS+fjijmziniNqw6h\n" +
+ "MBpyK4S/cM6PxV28C33VuOWPTMcIYesctjZANWFCggfFTQSjV5Qaxq9UK4i27tayLbCdlVS6hpbl\n" +
+ "Vf4DuI3Gj1Pv1rtITBShtvCf3T7yBnjW4wVpOpsUAAOViKUSvUU3kPPMFWhiGQw8yHYr82ts6XMo\n" +
+ "jwMoonW5Ml4e7C7Cr22QesC63q7emNcpUd0pZGT9C33RgDAHZDMrlyjo4HEp1JbUfB0gbmXElJbE\n" +
+ "1TNdZ62HhgmMjzTUN1GGrQ1t91AEoEQwaK65o4YSj+yFv6KXZZz5OWaz94tKiN9v26EXtBFmRlyb\n" +
+ "6+D9ynSd9LghAgMBAAECggEBANPHLRXkhsHVj1EkzqBx7gXr8CEMmiTvknFh9zvltrZhhDoRQjWr\n" +
+ "chPDkcRHY2Cznvy4N0YyqQDD2ULIlZdSAgPxxothFoBruWSD47yMBmLx08ORsDpcqt/YvPAATJI8\n" +
+ "IpFNsXcyaXBp/M57oRemgnxp/8UJPJmFdWX99H4hvffh/jdj7POgYiWUaAl37XTYZKZ4nzKU2wpL\n" +
+ "EDLj9RKPz9gG7CYp2zrLC9LaAsrXVrKwPBw6g+XwbClaqFj97db3mrY4lr6mTo89qmus1AU+fBDH\n" +
+ "3Xlpmc8JwB+30TvhRNKrpLx9cEjuEj7K1gm8Y4dWCjPi+lNbtAyUBcgPJFa/81ECgYEA7pLoBU/Y\n" +
+ "ZYjyHFca8FvDBcBh6haHfqJr9doXWtgjDrbi3o2n5wHqfKhFWOH6vPEQozkOVeX1ze6HOiRmGBpW\n" +
+ "r+r7x8TD25L7I6HJw3M351RWOAfkF0w/RTVdetcTgduQtfN1u6BDhYSVceXMjyQYx7MhfETWI8Gh\n" +
+ "KSYm8OEDYiUCgYEA489fmbrCcUnXzpTsbswJ5NmSoEXbcX8cLxnQuzE0z9GHhQdrMjOpXR76reTW\n" +
+ "6jcuudarNcwRUYSWWhjCDKHhpx4HhasWPaHgr7jIzcRw8yZSJRSxKr8sl1qh6g7s47JcmfXOMWLt\n" +
+ "yuyE933XrT19Th4ODZHY40Uv35mPjMi9d00CgYEAyRNAQtndBRa7GG/B4Ls2T+6pl+aNJIo4e+no\n" +
+ "rURlp800wWabEPRocdBRQmyULBLxduBr2LIMzhgwGSz8b2wji/l9ZA3PFY135bxClVzSzUIjuO3N\n" +
+ "rGUzHl2wAAyuAFDSUshzfkPBJRNt8aVBF5PQ3t93ZYmPAmv8LPZe875yX5ECgYEAsUEcwK/ZNW7g\n" +
+ "dQPZR4iJNkC4Xu6cBZ6Cnn92swBheEYvLSoNlX0vDZ7aLE3/jzQqrjzC8NP8sbH5jtbuvgeDXZX3\n" +
+ "AmGRp5j6C6A61ihAPmEVz3ZfN8SSfJ3vl//PAIg6lyz0J+cy4Q7RkwSeuVQ72Hl4M8TEvmmKC3Af\n" +
+ "ispy6Y0CgYEAgl1o2lo+ACyk+oVQPaaPqK3d7WOBFp4eR2nXFor/vsx9igQOlZUgzRDQsR8jo1o9\n" +
+ "efOSBf87igrZGgssys89pWa2dnXnz5PMmzkKr6bw4D9Ez6u6Puc9UZhGw/8wDYg6fSosdB9utspm\n" +
+ "M698ycef7jBNMDgmhpSvfw5GctoNQ4s=\n" +
+ "-----END RSA PRIVATE KEY-----\n" +
+ "</key>\n" +
+ "<cert>\n" +
+ "-----BEGIN CERTIFICATE-----\n" +
+ "MIIEjDCCAnSgAwIBAgIQG6MBp/cd9DlY+7cdvp3R3jANBgkqhkiG9w0BAQsFADBmMRAwDgYDVQQK\n" +
+ "DAdCaXRtYXNrMRwwGgYDVQQLDBNodHRwczovL2JpdG1hc2submV0MTQwMgYDVQQDDCtCaXRtYXNr\n" +
+ "IFJvb3QgQ0EgKGNsaWVudCBjZXJ0aWZpY2F0ZXMgb25seSEpMB4XDTE0MTIwNTAwMDAwMFoXDTE1\n" +
+ "MDMwNTAwMDAwMFowLTErMCkGA1UEAwwiVU5MSU1JVEVEZDBwZDdkMzE4eTNtOHNkeXllaTFqYmZl\n" +
+ "eDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANRNhZ4aCwdL5+OKObOKeI2rDqEwGnIr\n" +
+ "hL9wzo/FXbwLfdW45Y9Mxwhh6xy2NkA1YUKCB8VNBKNXlBrGr1QriLbu1rItsJ2VVLqGluVV/gO4\n" +
+ "jcaPU+/Wu0hMFKG28J/dPvIGeNbjBWk6mxQAA5WIpRK9RTeQ88wVaGIZDDzIdivza2zpcyiPAyii\n" +
+ "dbkyXh7sLsKvbZB6wLrert6Y1ylR3SlkZP0LfdGAMAdkMyuXKOjgcSnUltR8HSBuZcSUlsTVM11n\n" +
+ "rYeGCYyPNNQ3UYatDW33UASgRDBorrmjhhKP7IW/opdlnPk5ZrP3i0qI32/boRe0EWZGXJvr4P3K\n" +
+ "dJ30uCECAwEAAaNvMG0wHQYDVR0OBBYEFK8bMVAM4GBB5sHptoIOAaIvlYueMAsGA1UdDwQEAwIH\n" +
+ "gDATBgNVHSUEDDAKBggrBgEFBQcDAjAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFId+E7bsWFsUWah9\n" +
+ "vZuPvZ7O+aJsMA0GCSqGSIb3DQEBCwUAA4ICAQAQOX81csVhvP422NKkZH7+g3npBpl+sEHedaGR\n" +
+ "xYPOu4HrA4TVF9h44sljRoRJyenGNdBZCXcLKHg889eePTf8Z5K3lTojp6hvwyA6tgxOMHT1kESW\n" +
+ "PfqnRw8mHfHJuE3g+4YNUMwggzwc/VZATdV/7M33sarVN9AUOHou9n9BizgCC+UnYlS+F2POumE3\n" +
+ "FbOhKo5uubI02MwBYlN2JVO2TBt1Q20w8wc6cU07Xi5Epp+1mkgFiOShkNtPcJmEyBWJhxDtSDOW\n" +
+ "2doqWYNqH2kq7B5R/kyyfcpFJqAnBTV7xs+C5rTS1mW7LpxfdCUMbYuLCpyxpO3A/DhAm8n47tUH\n" +
+ "lBtmo8Avdb8VdFpYiGBpB0o9kTFcsWFb2GkWFBduGfSEB8jUI7QtqhgZqocAKK/cweSRV8FwyUcn\n" +
+ "R0prRm3QEi9fbXqEddzjSY9y/lqWYzT7u+IOAQpKroeZ4wzgYperDNOUFuYk1rP7yuvjP2pV5rcN\n" +
+ "yPoBP60TPVWMRM4WJm6nTogAz2qBrFsf/XwT/ajzbsjT6HNB7QbRE+wkFkqspoXG5Agp7KQ8lW3L\n" +
+ "SKCDGOQJz7VIE85pD0tg7QEXBEw8oaRZtMjQ0Gvs25mxXAKka4wGasaWfYH6d0E+iKYcWn86V1rH\n" +
+ "K2ZoknT+Nno5jgjFuUR3fZseNizEfx7BteooKQ==\n" +
+ "-----END CERTIFICATE-----\n" +
+ "</cert>\n" +
+ "crl-verify file missing in config profile\n" +
+ "remote-cert-tls server\n" +
+ "cipher AES-128-CBC\n" +
+ "auth SHA1\n" +
+ "persist-tun\n" +
+ "# persist-tun also enables pre resolving to avoid DNS resolve problem\n" +
+ "preresolve\n" +
+ "# Custom configuration options\n" +
+ "# You are on your on own here :)\n" +
+ "# These options found in the config file do not map to config settings:\n" +
+ "keepalive 10 30 \n" +
+ "tls-cipher DHE-RSA-AES128-SHA \n";
@Before
public void setUp() throws Exception {
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("general_configuration.json")));
secrets = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("secrets.json")));
+ mockStatic(Log.class);
+ context = MockHelper.mockContext();
+ mockTextUtils();
+ mockStatic(PreferenceManager.class);
+ SharedPreferences preferences = mock(SharedPreferences.class, RETURNS_DEEP_STUBS);
+ when(PreferenceManager.getDefaultSharedPreferences(any(Context.class))).thenReturn(preferences);
+ when(context.getCacheDir()).thenReturn(new File("/data/data/se.leap.bitmask"));
}
+
@Test
- public void testGenerate_tcp_udp() throws Exception {
+ public void testGenerateVpnProfile_v1_tcp_udp() throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_tcp_udp.json")));
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway);
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, 1);
+ HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertFalse(vpnProfiles.containsKey(OBFS4));
+ assertTrue(vpnProfiles.get(OPENVPN).getConfigFile(context, false).trim().equals(expectedVPNConfig_v1_tcp_udp.trim()));
+ }
- String vpnConfig = vpnConfigGenerator.generate();
- assertTrue(vpnConfig.equals(expectedVPNConfig_tcp_udp));
+ @Test
+ public void testGenerateVpnProfile_v1_udp_tcp() throws Exception {
+ gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_udp_tcp.json")));
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, 1);
+ HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertFalse(vpnProfiles.containsKey(OBFS4));
+ assertTrue(vpnProfiles.get(OPENVPN).getConfigFile(context, false).trim().equals(expectedVPNConfig_v1_udp_tcp.trim()));
+ }
+
+ @Test
+ public void testGenerateVpnProfile_v2_tcp_udp() throws Exception {
+ gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_tcp_udp.json")));
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, 2);
+ HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertFalse(vpnProfiles.containsKey(OBFS4));
+ assertTrue(vpnProfiles.get(OPENVPN).getConfigFile(context, false).trim().equals(expectedVPNConfig_v1_tcp_udp.trim()));
}
@Test
- public void testGenerate_udp_tcp() throws Exception {
+ public void testGenerateVpnProfile_v2_udp_tcp() throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_udp_tcp.json")));
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway);
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, 2);
+ HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertFalse(vpnProfiles.containsKey(OBFS4));
+ assertTrue(vpnProfiles.get(OPENVPN).getConfigFile(context, false).trim().equals(expectedVPNConfig_v1_udp_tcp.trim()));
+ }
+
- String vpnConfig = vpnConfigGenerator.generate();
- assertTrue(vpnConfig.equals(expectedVPNConfig_udp_tcp));
+ @Test
+ public void testGenerateVpnProfile_v3_obfs4() throws Exception {
+ gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo.bitmask.eip-service.json"))).getJSONArray("gateways").getJSONObject(0);
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, 3);
+ HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(vpnProfiles.containsKey(OBFS4));
+ assertTrue(vpnProfiles.containsKey(OPENVPN));
+ System.out.println(vpnProfiles.get(OBFS4).getConfigFile(context, false));
+ assertTrue(vpnProfiles.get(OBFS4).getConfigFile(context, false).trim().equals(expectedVPNConfig_v3_obfs4.trim()));
}
+ @Test
+ public void testGenerateVpnProfile_v3_ovpn_tcp_udp() throws Exception {
+ gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_pt_tcp_udp.eip-service.json"))).getJSONArray("gateways").getJSONObject(0);
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, 3);
+ HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(vpnProfiles.containsKey(OBFS4));
+ assertTrue(vpnProfiles.containsKey(OPENVPN));
+ System.out.println(vpnProfiles.get(OPENVPN).getConfigFile(context, false));
+ assertTrue(vpnProfiles.get(OPENVPN).getConfigFile(context, false).trim().equals(expectedVPNConfig_v3_ovpn_tcp_udp.trim()));
+ }
+
+ @Test
+ public void testGenerateVpnProfile_v3_ovpn_udp_tcp() throws Exception {
+ gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_pt_udp_tcp.eip-service.json"))).getJSONArray("gateways").getJSONObject(0);
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, 3);
+ HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(vpnProfiles.containsKey(OBFS4));
+ assertTrue(vpnProfiles.containsKey(OPENVPN));
+ System.out.println(vpnProfiles.get(OPENVPN).getConfigFile(context, false));
+ assertTrue(vpnProfiles.get(OPENVPN).getConfigFile(context, false).trim().equals(expectedVPNConfig_v3_ovpn_udp_tcp.trim()));
+ }
} \ No newline at end of file
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BackendMockProvider.java b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BackendMockProvider.java
index 307b61fc..a10b1414 100644
--- a/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BackendMockProvider.java
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BackendMockProvider.java
@@ -31,6 +31,7 @@ public class BackendMockProvider {
NO_ERROR,
ERROR_CASE_UPDATED_CERTIFICATE,
ERROR_CASE_MICONFIGURED_PROVIDER,
+ ERROR_CASE_FETCH_EIP_SERVICE_CERTIFICATE_INVALID,
ERROR_NO_RESPONSE_BODY, // => NullPointerException
ERROR_DNS_RESOLUTION_ERROR, // => UnkownHostException
ERROR_SOCKET_TIMEOUT, // => SocketTimeoutException
@@ -59,6 +60,8 @@ public class BackendMockProvider {
case ERROR_CASE_MICONFIGURED_PROVIDER:
new MisconfiguredProviderBackendResponse();
break;
+ case ERROR_CASE_FETCH_EIP_SERVICE_CERTIFICATE_INVALID:
+ new EipSerivceJsonInvalidCertificateBackendResponse();
case ERROR_NO_RESPONSE_BODY:
break;
case ERROR_DNS_RESOLUTION_ERROR:
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/EipSerivceJsonInvalidCertificateBackendResponse.java b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/EipSerivceJsonInvalidCertificateBackendResponse.java
new file mode 100644
index 00000000..b84c5508
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/EipSerivceJsonInvalidCertificateBackendResponse.java
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.testutils.BackendMockResponses;
+
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.IOException;
+
+import javax.net.ssl.SSLHandshakeException;
+
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.getInputAsString;
+
+/**
+ * Created by cyberta on 10.01.18.
+ */
+
+public class EipSerivceJsonInvalidCertificateBackendResponse extends BaseBackendResponse {
+ public EipSerivceJsonInvalidCertificateBackendResponse() throws IOException {
+ super();
+ }
+
+ @Override
+ public Answer<String> getAnswerForRequestStringFromServer() {
+ return new Answer<String>() {
+ @Override
+ public String answer(InvocationOnMock invocation) throws Throwable {
+ String url = (String) invocation.getArguments()[0];
+ String requestMethod = (String) invocation.getArguments()[1];
+ String jsonPayload = (String) invocation.getArguments()[2];
+
+ if (url.contains("/provider.json")) {
+ //download provider json
+ return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"));
+ } else if (url.contains("/ca.crt")) {
+ //download provider ca cert
+ return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem"));
+ } else if (url.contains("config/eip-service.json")) {
+ // download provider service json containing gateways, locations and openvpn settings
+ throw new SSLHandshakeException("Invalid provider CA certificate");
+ } else if (url.contains("/users.json")) {
+ //create new user
+ //TODO: implement me
+ } else if (url.contains("/sessions.json")) {
+ //srp auth: sendAToSRPServer
+ //TODO: implement me
+ } else if (url.contains("/sessions/parmegvtest10.json")){
+ //srp auth: sendM1ToSRPServer
+ //TODO: implement me
+ }
+
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public Answer<Boolean> getAnswerForCanConnect() {
+ return new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ return true;
+ }
+ };
+ }
+
+ @Override
+ public Answer<Boolean> getAnswerForDelete() {
+ return new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ return true;
+ }
+ };
+ }
+
+}
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 fa9f9252..24801b58 100644
--- a/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java
@@ -1,7 +1,10 @@
package se.leap.bitmaskclient.testutils;
+import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Parcelable;
@@ -460,4 +463,13 @@ public class MockHelper {
thenReturn("Bitmask");
return mockedResources;
}
+
+ public static Context mockContext() throws PackageManager.NameNotFoundException {
+ Context context = mock(Context.class, RETURNS_DEEP_STUBS);
+ when(context.getPackageName()).thenReturn("se.leap.bitmaskclient");
+ PackageInfo mockPackageInfo = new PackageInfo();
+ mockPackageInfo.versionName = "0.9.10";
+ when(context.getPackageManager().getPackageInfo(anyString(), anyInt())).thenReturn(mockPackageInfo);
+ return context;
+ }
}
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java b/app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java
index 49a44038..0604d5eb 100644
--- a/app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java
@@ -73,7 +73,7 @@ public class BundleMatcher extends BaseMatcher<Bundle> {
if (unfoundExpectedInteger.get(key) == null) {
description.appendText("\n unfound Integer in actual Bundle: ").appendValue(iterator.next());
} else {
- description.appendText("\n expected Integer for key " + key + ": ").appendValue(expectedIntegers.get(key)).
+ description.appendText("\n expected Integer for key \"" + key + "\": ").appendValue(expectedIntegers.get(key)).
appendText("\n found Integer was: ").appendValue(unfoundExpectedInteger.get(key));
}
}
@@ -85,7 +85,7 @@ public class BundleMatcher extends BaseMatcher<Bundle> {
if (unfoundExpectedBoolean.get(key) == null) {
description.appendText("\n unfound Boolean in actual Bundle: ").appendValue(iterator.next());
} else {
- description.appendText("\n expected Boolean for key " + key + ": ").appendValue(expectedBooleans.get(key)).
+ description.appendText("\n expected Boolean for key \"" + key + "\": ").appendValue(expectedBooleans.get(key)).
appendText("\n found Boolean was: ").appendValue(unfoundExpectedBoolean.get(key));
}
}
@@ -97,8 +97,8 @@ public class BundleMatcher extends BaseMatcher<Bundle> {
if (unfoundExpectedString.get(key) == null) {
description.appendText("\n unfound String in actual Bundle: ").appendValue(iterator.next());
} else {
- description.appendText("\n expected String for key " + key + ": ").appendValue(expectedStrings.get(key)).
- appendText("\n found String was: ").appendValue(unfoundExpectedString.get(key));
+ description.appendText("\n expected String for key \"" + key + "\": ").appendValue(expectedStrings.get(key)).
+ appendText("\n but found String was: ").appendValue(unfoundExpectedString.get(key));
}
}
}
@@ -109,7 +109,7 @@ public class BundleMatcher extends BaseMatcher<Bundle> {
if (unfoundExpectedParcelable.get(key) == null) {
description.appendText("\n unfound Parcelable in actual Bundle: ").appendValue(iterator.next());
} else {
- description.appendText("\n expected Parcelable or key " + key + ": ").appendValue(expectedParcelables.get(key)).
+ description.appendText("\n expected Parcelable or key \"" + key + "\": ").appendValue(expectedParcelables.get(key)).
appendText("\n found Parcelable was: ").appendValue(unfoundExpectedParcelable.get(key));
}
}
diff --git a/app/src/test/resources/openvpnConfigs/tcp_udp.ovpn b/app/src/test/resources/openvpnConfigs/tcp_udp.ovpn
new file mode 100644
index 00000000..875fe606
--- /dev/null
+++ b/app/src/test/resources/openvpnConfigs/tcp_udp.ovpn
@@ -0,0 +1,117 @@
+# Config for OpenVPN 2.x
+# Enables connection to GUI
+management /data/data/se.leap.bitmask/mgmtsocket unix
+management-client
+management-query-passwords
+management-hold
+
+setenv IV_GUI_VER "se.leap.bitmaskclient 0.9.10"
+setenv IV_PLAT_VER "0 null JUNIT null null null"
+machine-readable-output
+allow-recursive-routing
+ifconfig-nowarn
+client
+verb 4
+connect-retry 2 300
+resolv-retry 60
+dev tun
+remote 198.252.153.84 443 tcp-client
+remote 198.252.153.84 443 udp
+<ca>
+-----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-----
+
+</ca>
+<key>
+-----BEGIN RSA PRIVATE KEY-----
+MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDUTYWeGgsHS+fjijmziniNqw6h
+MBpyK4S/cM6PxV28C33VuOWPTMcIYesctjZANWFCggfFTQSjV5Qaxq9UK4i27tayLbCdlVS6hpbl
+Vf4DuI3Gj1Pv1rtITBShtvCf3T7yBnjW4wVpOpsUAAOViKUSvUU3kPPMFWhiGQw8yHYr82ts6XMo
+jwMoonW5Ml4e7C7Cr22QesC63q7emNcpUd0pZGT9C33RgDAHZDMrlyjo4HEp1JbUfB0gbmXElJbE
+1TNdZ62HhgmMjzTUN1GGrQ1t91AEoEQwaK65o4YSj+yFv6KXZZz5OWaz94tKiN9v26EXtBFmRlyb
+6+D9ynSd9LghAgMBAAECggEBANPHLRXkhsHVj1EkzqBx7gXr8CEMmiTvknFh9zvltrZhhDoRQjWr
+chPDkcRHY2Cznvy4N0YyqQDD2ULIlZdSAgPxxothFoBruWSD47yMBmLx08ORsDpcqt/YvPAATJI8
+IpFNsXcyaXBp/M57oRemgnxp/8UJPJmFdWX99H4hvffh/jdj7POgYiWUaAl37XTYZKZ4nzKU2wpL
+EDLj9RKPz9gG7CYp2zrLC9LaAsrXVrKwPBw6g+XwbClaqFj97db3mrY4lr6mTo89qmus1AU+fBDH
+3Xlpmc8JwB+30TvhRNKrpLx9cEjuEj7K1gm8Y4dWCjPi+lNbtAyUBcgPJFa/81ECgYEA7pLoBU/Y
+ZYjyHFca8FvDBcBh6haHfqJr9doXWtgjDrbi3o2n5wHqfKhFWOH6vPEQozkOVeX1ze6HOiRmGBpW
+r+r7x8TD25L7I6HJw3M351RWOAfkF0w/RTVdetcTgduQtfN1u6BDhYSVceXMjyQYx7MhfETWI8Gh
+KSYm8OEDYiUCgYEA489fmbrCcUnXzpTsbswJ5NmSoEXbcX8cLxnQuzE0z9GHhQdrMjOpXR76reTW
+6jcuudarNcwRUYSWWhjCDKHhpx4HhasWPaHgr7jIzcRw8yZSJRSxKr8sl1qh6g7s47JcmfXOMWLt
+yuyE933XrT19Th4ODZHY40Uv35mPjMi9d00CgYEAyRNAQtndBRa7GG/B4Ls2T+6pl+aNJIo4e+no
+rURlp800wWabEPRocdBRQmyULBLxduBr2LIMzhgwGSz8b2wji/l9ZA3PFY135bxClVzSzUIjuO3N
+rGUzHl2wAAyuAFDSUshzfkPBJRNt8aVBF5PQ3t93ZYmPAmv8LPZe875yX5ECgYEAsUEcwK/ZNW7g
+dQPZR4iJNkC4Xu6cBZ6Cnn92swBheEYvLSoNlX0vDZ7aLE3/jzQqrjzC8NP8sbH5jtbuvgeDXZX3
+AmGRp5j6C6A61ihAPmEVz3ZfN8SSfJ3vl//PAIg6lyz0J+cy4Q7RkwSeuVQ72Hl4M8TEvmmKC3Af
+ispy6Y0CgYEAgl1o2lo+ACyk+oVQPaaPqK3d7WOBFp4eR2nXFor/vsx9igQOlZUgzRDQsR8jo1o9
+efOSBf87igrZGgssys89pWa2dnXnz5PMmzkKr6bw4D9Ez6u6Puc9UZhGw/8wDYg6fSosdB9utspm
+M698ycef7jBNMDgmhpSvfw5GctoNQ4s=
+-----END RSA PRIVATE KEY-----
+</key>
+<cert>
+-----BEGIN CERTIFICATE-----
+MIIEjDCCAnSgAwIBAgIQG6MBp/cd9DlY+7cdvp3R3jANBgkqhkiG9w0BAQsFADBmMRAwDgYDVQQK
+DAdCaXRtYXNrMRwwGgYDVQQLDBNodHRwczovL2JpdG1hc2submV0MTQwMgYDVQQDDCtCaXRtYXNr
+IFJvb3QgQ0EgKGNsaWVudCBjZXJ0aWZpY2F0ZXMgb25seSEpMB4XDTE0MTIwNTAwMDAwMFoXDTE1
+MDMwNTAwMDAwMFowLTErMCkGA1UEAwwiVU5MSU1JVEVEZDBwZDdkMzE4eTNtOHNkeXllaTFqYmZl
+eDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANRNhZ4aCwdL5+OKObOKeI2rDqEwGnIr
+hL9wzo/FXbwLfdW45Y9Mxwhh6xy2NkA1YUKCB8VNBKNXlBrGr1QriLbu1rItsJ2VVLqGluVV/gO4
+jcaPU+/Wu0hMFKG28J/dPvIGeNbjBWk6mxQAA5WIpRK9RTeQ88wVaGIZDDzIdivza2zpcyiPAyii
+dbkyXh7sLsKvbZB6wLrert6Y1ylR3SlkZP0LfdGAMAdkMyuXKOjgcSnUltR8HSBuZcSUlsTVM11n
+rYeGCYyPNNQ3UYatDW33UASgRDBorrmjhhKP7IW/opdlnPk5ZrP3i0qI32/boRe0EWZGXJvr4P3K
+dJ30uCECAwEAAaNvMG0wHQYDVR0OBBYEFK8bMVAM4GBB5sHptoIOAaIvlYueMAsGA1UdDwQEAwIH
+gDATBgNVHSUEDDAKBggrBgEFBQcDAjAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFId+E7bsWFsUWah9
+vZuPvZ7O+aJsMA0GCSqGSIb3DQEBCwUAA4ICAQAQOX81csVhvP422NKkZH7+g3npBpl+sEHedaGR
+xYPOu4HrA4TVF9h44sljRoRJyenGNdBZCXcLKHg889eePTf8Z5K3lTojp6hvwyA6tgxOMHT1kESW
+PfqnRw8mHfHJuE3g+4YNUMwggzwc/VZATdV/7M33sarVN9AUOHou9n9BizgCC+UnYlS+F2POumE3
+FbOhKo5uubI02MwBYlN2JVO2TBt1Q20w8wc6cU07Xi5Epp+1mkgFiOShkNtPcJmEyBWJhxDtSDOW
+2doqWYNqH2kq7B5R/kyyfcpFJqAnBTV7xs+C5rTS1mW7LpxfdCUMbYuLCpyxpO3A/DhAm8n47tUH
+lBtmo8Avdb8VdFpYiGBpB0o9kTFcsWFb2GkWFBduGfSEB8jUI7QtqhgZqocAKK/cweSRV8FwyUcn
+R0prRm3QEi9fbXqEddzjSY9y/lqWYzT7u+IOAQpKroeZ4wzgYperDNOUFuYk1rP7yuvjP2pV5rcN
+yPoBP60TPVWMRM4WJm6nTogAz2qBrFsf/XwT/ajzbsjT6HNB7QbRE+wkFkqspoXG5Agp7KQ8lW3L
+SKCDGOQJz7VIE85pD0tg7QEXBEw8oaRZtMjQ0Gvs25mxXAKka4wGasaWfYH6d0E+iKYcWn86V1rH
+K2ZoknT+Nno5jgjFuUR3fZseNizEfx7BteooKQ==
+-----END CERTIFICATE-----
+</cert>
+crl-verify file missing in config profile
+remote-cert-tls server
+cipher AES-128-CBC
+auth SHA1
+persist-tun
+# persist-tun also enables pre resolving to avoid DNS resolve problem
+preresolve
+# Custom configuration options
+# You are on your on own here :)
+# These options found in the config file do not map to config settings:
+keepalive 10 30
+tls-cipher DHE-RSA-AES128-SHA \ No newline at end of file
diff --git a/app/src/test/resources/openvpnConfigs/udp_tcp.ovpn b/app/src/test/resources/openvpnConfigs/udp_tcp.ovpn
new file mode 100644
index 00000000..300d265e
--- /dev/null
+++ b/app/src/test/resources/openvpnConfigs/udp_tcp.ovpn
@@ -0,0 +1,117 @@
+# Config for OpenVPN 2.x
+# Enables connection to GUI
+management /data/data/se.leap.bitmask/mgmtsocket unix
+management-client
+management-query-passwords
+management-hold
+
+setenv IV_GUI_VER "se.leap.bitmaskclient 0.9.10"
+setenv IV_PLAT_VER "0 null JUNIT null null null"
+machine-readable-output
+allow-recursive-routing
+ifconfig-nowarn
+client
+verb 4
+connect-retry 2 300
+resolv-retry 60
+dev tun
+remote 198.252.153.84 443 udp
+remote 198.252.153.84 443 tcp-client
+<ca>
+-----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-----
+
+</ca>
+<key>
+-----BEGIN RSA PRIVATE KEY-----
+MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDUTYWeGgsHS+fjijmziniNqw6h
+MBpyK4S/cM6PxV28C33VuOWPTMcIYesctjZANWFCggfFTQSjV5Qaxq9UK4i27tayLbCdlVS6hpbl
+Vf4DuI3Gj1Pv1rtITBShtvCf3T7yBnjW4wVpOpsUAAOViKUSvUU3kPPMFWhiGQw8yHYr82ts6XMo
+jwMoonW5Ml4e7C7Cr22QesC63q7emNcpUd0pZGT9C33RgDAHZDMrlyjo4HEp1JbUfB0gbmXElJbE
+1TNdZ62HhgmMjzTUN1GGrQ1t91AEoEQwaK65o4YSj+yFv6KXZZz5OWaz94tKiN9v26EXtBFmRlyb
+6+D9ynSd9LghAgMBAAECggEBANPHLRXkhsHVj1EkzqBx7gXr8CEMmiTvknFh9zvltrZhhDoRQjWr
+chPDkcRHY2Cznvy4N0YyqQDD2ULIlZdSAgPxxothFoBruWSD47yMBmLx08ORsDpcqt/YvPAATJI8
+IpFNsXcyaXBp/M57oRemgnxp/8UJPJmFdWX99H4hvffh/jdj7POgYiWUaAl37XTYZKZ4nzKU2wpL
+EDLj9RKPz9gG7CYp2zrLC9LaAsrXVrKwPBw6g+XwbClaqFj97db3mrY4lr6mTo89qmus1AU+fBDH
+3Xlpmc8JwB+30TvhRNKrpLx9cEjuEj7K1gm8Y4dWCjPi+lNbtAyUBcgPJFa/81ECgYEA7pLoBU/Y
+ZYjyHFca8FvDBcBh6haHfqJr9doXWtgjDrbi3o2n5wHqfKhFWOH6vPEQozkOVeX1ze6HOiRmGBpW
+r+r7x8TD25L7I6HJw3M351RWOAfkF0w/RTVdetcTgduQtfN1u6BDhYSVceXMjyQYx7MhfETWI8Gh
+KSYm8OEDYiUCgYEA489fmbrCcUnXzpTsbswJ5NmSoEXbcX8cLxnQuzE0z9GHhQdrMjOpXR76reTW
+6jcuudarNcwRUYSWWhjCDKHhpx4HhasWPaHgr7jIzcRw8yZSJRSxKr8sl1qh6g7s47JcmfXOMWLt
+yuyE933XrT19Th4ODZHY40Uv35mPjMi9d00CgYEAyRNAQtndBRa7GG/B4Ls2T+6pl+aNJIo4e+no
+rURlp800wWabEPRocdBRQmyULBLxduBr2LIMzhgwGSz8b2wji/l9ZA3PFY135bxClVzSzUIjuO3N
+rGUzHl2wAAyuAFDSUshzfkPBJRNt8aVBF5PQ3t93ZYmPAmv8LPZe875yX5ECgYEAsUEcwK/ZNW7g
+dQPZR4iJNkC4Xu6cBZ6Cnn92swBheEYvLSoNlX0vDZ7aLE3/jzQqrjzC8NP8sbH5jtbuvgeDXZX3
+AmGRp5j6C6A61ihAPmEVz3ZfN8SSfJ3vl//PAIg6lyz0J+cy4Q7RkwSeuVQ72Hl4M8TEvmmKC3Af
+ispy6Y0CgYEAgl1o2lo+ACyk+oVQPaaPqK3d7WOBFp4eR2nXFor/vsx9igQOlZUgzRDQsR8jo1o9
+efOSBf87igrZGgssys89pWa2dnXnz5PMmzkKr6bw4D9Ez6u6Puc9UZhGw/8wDYg6fSosdB9utspm
+M698ycef7jBNMDgmhpSvfw5GctoNQ4s=
+-----END RSA PRIVATE KEY-----
+</key>
+<cert>
+-----BEGIN CERTIFICATE-----
+MIIEjDCCAnSgAwIBAgIQG6MBp/cd9DlY+7cdvp3R3jANBgkqhkiG9w0BAQsFADBmMRAwDgYDVQQK
+DAdCaXRtYXNrMRwwGgYDVQQLDBNodHRwczovL2JpdG1hc2submV0MTQwMgYDVQQDDCtCaXRtYXNr
+IFJvb3QgQ0EgKGNsaWVudCBjZXJ0aWZpY2F0ZXMgb25seSEpMB4XDTE0MTIwNTAwMDAwMFoXDTE1
+MDMwNTAwMDAwMFowLTErMCkGA1UEAwwiVU5MSU1JVEVEZDBwZDdkMzE4eTNtOHNkeXllaTFqYmZl
+eDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANRNhZ4aCwdL5+OKObOKeI2rDqEwGnIr
+hL9wzo/FXbwLfdW45Y9Mxwhh6xy2NkA1YUKCB8VNBKNXlBrGr1QriLbu1rItsJ2VVLqGluVV/gO4
+jcaPU+/Wu0hMFKG28J/dPvIGeNbjBWk6mxQAA5WIpRK9RTeQ88wVaGIZDDzIdivza2zpcyiPAyii
+dbkyXh7sLsKvbZB6wLrert6Y1ylR3SlkZP0LfdGAMAdkMyuXKOjgcSnUltR8HSBuZcSUlsTVM11n
+rYeGCYyPNNQ3UYatDW33UASgRDBorrmjhhKP7IW/opdlnPk5ZrP3i0qI32/boRe0EWZGXJvr4P3K
+dJ30uCECAwEAAaNvMG0wHQYDVR0OBBYEFK8bMVAM4GBB5sHptoIOAaIvlYueMAsGA1UdDwQEAwIH
+gDATBgNVHSUEDDAKBggrBgEFBQcDAjAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFId+E7bsWFsUWah9
+vZuPvZ7O+aJsMA0GCSqGSIb3DQEBCwUAA4ICAQAQOX81csVhvP422NKkZH7+g3npBpl+sEHedaGR
+xYPOu4HrA4TVF9h44sljRoRJyenGNdBZCXcLKHg889eePTf8Z5K3lTojp6hvwyA6tgxOMHT1kESW
+PfqnRw8mHfHJuE3g+4YNUMwggzwc/VZATdV/7M33sarVN9AUOHou9n9BizgCC+UnYlS+F2POumE3
+FbOhKo5uubI02MwBYlN2JVO2TBt1Q20w8wc6cU07Xi5Epp+1mkgFiOShkNtPcJmEyBWJhxDtSDOW
+2doqWYNqH2kq7B5R/kyyfcpFJqAnBTV7xs+C5rTS1mW7LpxfdCUMbYuLCpyxpO3A/DhAm8n47tUH
+lBtmo8Avdb8VdFpYiGBpB0o9kTFcsWFb2GkWFBduGfSEB8jUI7QtqhgZqocAKK/cweSRV8FwyUcn
+R0prRm3QEi9fbXqEddzjSY9y/lqWYzT7u+IOAQpKroeZ4wzgYperDNOUFuYk1rP7yuvjP2pV5rcN
+yPoBP60TPVWMRM4WJm6nTogAz2qBrFsf/XwT/ajzbsjT6HNB7QbRE+wkFkqspoXG5Agp7KQ8lW3L
+SKCDGOQJz7VIE85pD0tg7QEXBEw8oaRZtMjQ0Gvs25mxXAKka4wGasaWfYH6d0E+iKYcWn86V1rH
+K2ZoknT+Nno5jgjFuUR3fZseNizEfx7BteooKQ==
+-----END CERTIFICATE-----
+</cert>
+crl-verify file missing in config profile
+remote-cert-tls server
+cipher AES-128-CBC
+auth SHA1
+persist-tun
+# persist-tun also enables pre resolving to avoid DNS resolve problem
+preresolve
+# Custom configuration options
+# You are on your on own here :)
+# These options found in the config file do not map to config settings:
+keepalive 10 30
+tls-cipher DHE-RSA-AES128-SHA \ No newline at end of file
diff --git a/app/src/test/resources/ptdemo.bitmask.eip-service.json b/app/src/test/resources/ptdemo.bitmask.eip-service.json
new file mode 100644
index 00000000..e5ede239
--- /dev/null
+++ b/app/src/test/resources/ptdemo.bitmask.eip-service.json
@@ -0,0 +1,64 @@
+{
+ "gateways":[
+ {
+ "capabilities":{
+ "adblock":false,
+ "filter_dns":false,
+ "limited":false,
+ "transport":[
+ {
+ "type":"obfs4",
+ "protocols":[
+ "tcp"
+ ],
+ "ports":[
+ "23049"
+ ],
+ "options": {
+ "cert": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX1",
+ "iat-mode": "0"
+ }
+ },
+ {
+ "type":"openvpn",
+ "protocols":[
+ "tcp"
+ ],
+ "ports":[
+ "1195"
+ ]
+ }
+ ],
+ "user_ips":false
+ },
+ "host":"pt.demo.bitmask.net",
+ "ip_address":"37.218.247.60",
+ "location":"Amsterdam"
+ }
+ ],
+ "locations":{
+ "Amsterdam":{
+ "country_code":"NL",
+ "hemisphere":"N",
+ "name":"Amsterdam",
+ "timezone":"-1"
+ }
+ },
+ "openvpn_configuration":{
+ "auth":"SHA1",
+ "cipher":"AES-256-CBC",
+ "keepalive":"10 30",
+ "tls-cipher":"DHE-RSA-AES128-SHA",
+ "tun-ipv6":true,
+ "dev" : "tun",
+ "sndbuf" : "0",
+ "rcvbuf" : "0",
+ "nobind" : true,
+ "persist-key" : true,
+ "comp-lzo" : true,
+ "key-direction" : "1",
+ "verb" : "3"
+ },
+ "serial":2,
+ "version":3
+} \ No newline at end of file
diff --git a/app/src/test/resources/ptdemo_pt_tcp_udp.eip-service.json b/app/src/test/resources/ptdemo_pt_tcp_udp.eip-service.json
new file mode 100644
index 00000000..f39a1597
--- /dev/null
+++ b/app/src/test/resources/ptdemo_pt_tcp_udp.eip-service.json
@@ -0,0 +1,65 @@
+{
+ "gateways":[
+ {
+ "capabilities":{
+ "adblock":false,
+ "filter_dns":false,
+ "limited":false,
+ "transport":[
+ {
+ "type":"obfs4",
+ "protocols":[
+ "tcp"
+ ],
+ "ports":[
+ "23049"
+ ],
+ "options": {
+ "cert": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
+ "iat-mode": "0"
+ }
+ },
+ {
+ "type":"openvpn",
+ "protocols":[
+ "tcp",
+ "udp"
+ ],
+ "ports":[
+ "1195"
+ ]
+ }
+ ],
+ "user_ips":false
+ },
+ "host":"pt.demo.bitmask.net",
+ "ip_address":"37.218.247.60",
+ "location":"Amsterdam"
+ }
+ ],
+ "locations":{
+ "Amsterdam":{
+ "country_code":"NL",
+ "hemisphere":"N",
+ "name":"Amsterdam",
+ "timezone":"-1"
+ }
+ },
+ "openvpn_configuration":{
+ "auth":"SHA1",
+ "cipher":"AES-256-CBC",
+ "keepalive":"10 30",
+ "tls-cipher":"DHE-RSA-AES128-SHA",
+ "tun-ipv6":true,
+ "dev" : "tun",
+ "sndbuf" : "0",
+ "rcvbuf" : "0",
+ "nobind" : true,
+ "persist-key" : true,
+ "comp-lzo" : true,
+ "key-direction" : "1",
+ "verb" : "3"
+ },
+ "serial":2,
+ "version":3
+} \ No newline at end of file
diff --git a/app/src/test/resources/ptdemo_pt_udp_tcp.eip-service.json b/app/src/test/resources/ptdemo_pt_udp_tcp.eip-service.json
new file mode 100644
index 00000000..71c9857a
--- /dev/null
+++ b/app/src/test/resources/ptdemo_pt_udp_tcp.eip-service.json
@@ -0,0 +1,65 @@
+{
+ "gateways":[
+ {
+ "capabilities":{
+ "adblock":false,
+ "filter_dns":false,
+ "limited":false,
+ "transport":[
+ {
+ "type":"obfs4",
+ "protocols":[
+ "tcp"
+ ],
+ "ports":[
+ "23049"
+ ],
+ "options": {
+ "cert": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
+ "iat-mode": "0"
+ }
+ },
+ {
+ "type":"openvpn",
+ "protocols":[
+ "udp",
+ "tcp"
+ ],
+ "ports":[
+ "1195"
+ ]
+ }
+ ],
+ "user_ips":false
+ },
+ "host":"pt.demo.bitmask.net",
+ "ip_address":"37.218.247.60",
+ "location":"Amsterdam"
+ }
+ ],
+ "locations":{
+ "Amsterdam":{
+ "country_code":"NL",
+ "hemisphere":"N",
+ "name":"Amsterdam",
+ "timezone":"-1"
+ }
+ },
+ "openvpn_configuration":{
+ "auth":"SHA1",
+ "cipher":"AES-256-CBC",
+ "keepalive":"10 30",
+ "tls-cipher":"DHE-RSA-AES128-SHA",
+ "tun-ipv6":true,
+ "dev" : "tun",
+ "sndbuf" : "0",
+ "rcvbuf" : "0",
+ "nobind" : true,
+ "persist-key" : true,
+ "comp-lzo" : true,
+ "key-direction" : "1",
+ "verb" : "3"
+ },
+ "serial":2,
+ "version":3
+} \ No newline at end of file
diff --git a/app/src/test/resources/ptdemo_three_mixed_gateways.json b/app/src/test/resources/ptdemo_three_mixed_gateways.json
new file mode 100644
index 00000000..f81da6b3
--- /dev/null
+++ b/app/src/test/resources/ptdemo_three_mixed_gateways.json
@@ -0,0 +1,133 @@
+{
+ "gateways":[
+ {
+ "capabilities":{
+ "adblock":false,
+ "filter_dns":false,
+ "limited":false,
+ "transport":[
+ {
+ "type":"obfs4",
+ "protocols":[
+ "tcp"
+ ],
+ "ports":[
+ "23049"
+ ],
+ "options": {
+ "cert": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX1",
+ "iat-mode": "0"
+ }
+ },
+ {
+ "type":"openvpn",
+ "protocols":[
+ "tcp"
+ ],
+ "ports":[
+ "1195"
+ ]
+ }
+ ],
+ "user_ips":false
+ },
+ "host":"pt.demo.bitmask.net",
+ "ip_address":"37.218.247.60",
+ "location":"Amsterdam"
+ },
+ {
+ "capabilities":{
+ "adblock":false,
+ "filter_dns":false,
+ "limited":false,
+ "transport":[
+ {
+ "type":"obfs4",
+ "protocols":[
+ "tcp"
+ ],
+ "ports":[
+ "443"
+ ],
+ "options": {
+ "cert": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX2",
+ "iat-mode": "0"
+ }
+ },
+ {
+ "type":"openvpn",
+ "protocols":[
+ "tcp"
+ ],
+ "ports":[
+ "1195"
+ ]
+ }
+ ],
+ "user_ips":false
+ },
+ "host":"moscow.bitmask.net",
+ "ip_address":"3.21.247.89",
+ "location":"moscow"
+ },
+ {
+ "capabilities":{
+ "adblock":false,
+ "filter_dns":false,
+ "limited":false,
+ "transport":[
+ {
+ "type":"openvpn",
+ "protocols":[
+ "tcp",
+ "udp"
+ ],
+ "ports":[
+ "1195"
+ ]
+ }
+ ],
+ "user_ips":false
+ },
+ "host":"manila.bitmask.net",
+ "ip_address":"37.12.247.10",
+ "location":"manila"
+ }
+ ],
+ "locations":{
+ "Amsterdam":{
+ "country_code":"NL",
+ "hemisphere":"N",
+ "name":"Amsterdam",
+ "timezone":"-1"
+ },
+ "moscow": {
+ "country_code": "RU",
+ "hemisphere": "N",
+ "name": "Moscow",
+ "timezone": "+3"
+ },
+ "manila": {
+ "country_code": "PH",
+ "hemisphere": "N",
+ "name": "Manila",
+ "timezone": "+8"
+ }
+ },
+ "openvpn_configuration":{
+ "auth":"SHA1",
+ "cipher":"AES-256-CBC",
+ "keepalive":"10 30",
+ "tls-cipher":"DHE-RSA-AES128-SHA",
+ "tun-ipv6":true,
+ "dev" : "tun",
+ "sndbuf" : "0",
+ "rcvbuf" : "0",
+ "nobind" : true,
+ "persist-key" : true,
+ "key-direction" : "1",
+ "verb" : "3"
+ },
+ "serial":2,
+ "version":3
+} \ No newline at end of file