diff options
Diffstat (limited to 'app')
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 Binary files differnew file mode 100644 index 00000000..e3acd2d1 --- /dev/null +++ b/app/src/main/res/drawable-hdpi/ic_bridge_36.png diff --git a/app/src/main/res/drawable-mdpi/ic_bridge_36.png b/app/src/main/res/drawable-mdpi/ic_bridge_36.png Binary files differnew file mode 100644 index 00000000..6c45a2d8 --- /dev/null +++ b/app/src/main/res/drawable-mdpi/ic_bridge_36.png diff --git a/app/src/main/res/drawable-xhdpi/ic_bridge_36.png b/app/src/main/res/drawable-xhdpi/ic_bridge_36.png Binary files differnew file mode 100644 index 00000000..6f89408c --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/ic_bridge_36.png diff --git a/app/src/main/res/drawable-xxhdpi/ic_bridge_36.png b/app/src/main/res/drawable-xxhdpi/ic_bridge_36.png Binary files differnew file mode 100644 index 00000000..d00613b8 --- /dev/null +++ b/app/src/main/res/drawable-xxhdpi/ic_bridge_36.png diff --git a/app/src/main/res/drawable-xxxhdpi/ic_bridge_36.png b/app/src/main/res/drawable-xxxhdpi/ic_bridge_36.png Binary files differnew file mode 100644 index 00000000..8f531f5a --- /dev/null +++ b/app/src/main/res/drawable-xxxhdpi/ic_bridge_36.png 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 |