summaryrefslogtreecommitdiff
path: root/app/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/de/blinkt/openvpn/VpnProfile.java51
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java9
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java139
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java5
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/VpnStatus.java2
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java57
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java44
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4HopConnection.java48
-rw-r--r--app/src/main/java/io/swagger/client/JSON.java399
-rw-r--r--app/src/main/java/io/swagger/client/model/ModelsBridge.java428
-rw-r--r--app/src/main/java/io/swagger/client/model/ModelsEIPService.java206
-rw-r--r--app/src/main/java/io/swagger/client/model/ModelsGateway.java371
-rw-r--r--app/src/main/java/io/swagger/client/model/ModelsLocation.java301
-rw-r--r--app/src/main/java/io/swagger/client/model/ModelsProvider.java584
-rw-r--r--app/src/main/java/io/swagger/client/model/ModelsProviderService.java118
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java11
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java7
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/CensorshipCircumventionFragment.java166
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java5
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java8
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/LanguageSelectionFragment.java166
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java25
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/ObfuscationProxyDialog.java41
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/SettingsFragment.java126
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java14
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java1
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Introducer.java132
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java522
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java175
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/BitmaskCoreProvider.java28
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/BuildConfigHelper.java23
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java57
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/CredentialsParser.java58
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java2
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java242
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/PrivateKeyHelper.java124
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/RSAHelper.java72
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java6
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/EIP.java29
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java34
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java221
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java398
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java253
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/HoppingObfsVpnClient.java66
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsVpnClient.java144
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsvpnClient.java180
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/PtClientBuilder.java18
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/PtClientInterface.java9
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ShapeshifterClient.java143
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/HoppingConfig.java (renamed from app/src/main/java/se/leap/bitmaskclient/pluggableTransports/HoppingConfig.java)43
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/KcpConfig.java37
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/Obfs4Options.java (renamed from app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Obfs4Options.java)8
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/ObfsvpnConfig.java37
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/QuicConfig.java24
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/IProviderApiManager.java11
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java30
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiEventSender.java180
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManager.java167
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java1024
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerFactory.java25
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerV3.java765
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerV5.java354
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiSetupBroadcastReceiver.java8
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiTorHandler.java54
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java23
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java6
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupObservable.java34
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/SetupViewPagerAdapter.java13
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java77
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/CircumventionSetupFragment.java8
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ConfigureProviderFragment.java68
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/EmptyPermissionSetupFragment.java3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/NotificationSetupFragment.java35
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/PermissionExplanationFragment.java72
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java98
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/SetupFragmentFactory.java20
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/VpnPermissionSetupFragment.java36
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/helpers/AbstractQrScannerHelper.java16
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java61
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java30
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/TorServiceCommand.java15
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java10
84 files changed, 7233 insertions, 2433 deletions
diff --git a/app/src/main/java/de/blinkt/openvpn/VpnProfile.java b/app/src/main/java/de/blinkt/openvpn/VpnProfile.java
index 9da1e452..9e71939b 100644
--- a/app/src/main/java/de/blinkt/openvpn/VpnProfile.java
+++ b/app/src/main/java/de/blinkt/openvpn/VpnProfile.java
@@ -5,6 +5,8 @@
package de.blinkt.openvpn;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_HOP;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PROFILE;
import static se.leap.bitmaskclient.base.utils.ConfigHelper.stringEqual;
@@ -41,6 +43,7 @@ import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
@@ -50,6 +53,7 @@ import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
+import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
@@ -73,10 +77,12 @@ 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.ConnectionAdapter;
+import de.blinkt.openvpn.core.connection.Obfs4Connection;
import se.leap.bitmaskclient.BuildConfig;
import se.leap.bitmaskclient.R;
import se.leap.bitmaskclient.base.models.ProviderObservable;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import se.leap.bitmaskclient.pluggableTransports.models.Obfs4Options;
public class VpnProfile implements Serializable, Cloneable {
// Note that this class cannot be moved to core where it belongs since
@@ -272,11 +278,20 @@ public class VpnProfile implements Serializable, Cloneable {
}
@Override
+ public int hashCode() {
+ int result =(mGatewayIp != null ? mGatewayIp.hashCode() : 0);
+ result = 31 * result + Arrays.hashCode(mConnections);
+ result = 31 * result + mTransportType;
+ return result;
+ }
+
+ @Override
public boolean equals(Object obj) {
if (obj instanceof VpnProfile) {
VpnProfile vp = (VpnProfile) obj;
return stringEqual(vp.mGatewayIp, mGatewayIp) &&
- vp.mTransportType == mTransportType;
+ vp.mTransportType == mTransportType &&
+ Arrays.equals(mConnections, vp.mConnections);
}
return false;
}
@@ -315,6 +330,22 @@ public class VpnProfile implements Serializable, Cloneable {
return Connection.TransportType.fromInt(mTransportType);
}
+ public @Nullable Obfs4Options getObfs4Options() {
+ Connection.TransportType transportType = getTransportType();
+ if (!(transportType == OBFS4 || transportType == OBFS4_HOP)) {
+ return null;
+ }
+ return ((Obfs4Connection) mConnections[0]).getObfs4Options();
+ }
+
+ public String getObfuscationTransportLayerProtocol() {
+ try {
+ return getObfs4Options().transport.getProtocols()[0];
+ } catch (NullPointerException | ArrayIndexOutOfBoundsException ignore) {
+ return null;
+ }
+ }
+
public String getName() {
if (TextUtils.isEmpty(mName))
return "No profile name";
@@ -444,8 +475,12 @@ public class VpnProfile implements Serializable, Cloneable {
// Client Cert + Key
cfg.append(insertFileData("cert", mClientCertFilename));
- mPrivateKey = ProviderObservable.getInstance().getCurrentProvider().getRSAPrivateKey();
- cfg.append("management-external-key nopadding pkcs1 pss digest\n");
+ mPrivateKey = ProviderObservable.getInstance().getCurrentProvider().getPrivateKey();
+ if (mPrivateKey.getAlgorithm().equalsIgnoreCase("RSA")) {
+ cfg.append("management-external-key nopadding pkcs1 pss digest\n");
+ } else {
+ cfg.append("management-external-key\n");
+ }
break;
case VpnProfile.TYPE_USERPASS_PKCS12:
@@ -1250,7 +1285,9 @@ public class VpnProfile implements Serializable, Cloneable {
return signed_bytes;
}
} catch
- (NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException | SignatureException | InvalidAlgorithmParameterException
+ (NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException |
+ BadPaddingException | NoSuchPaddingException | SignatureException |
+ InvalidAlgorithmParameterException | NoSuchProviderException
e) {
VpnStatus.logError(R.string.error_rsa_sign, e.getClass().toString(), e.getLocalizedMessage());
return null;
@@ -1296,11 +1333,13 @@ public class VpnProfile implements Serializable, Cloneable {
return hashtype;
}
- private byte[] doDigestSign(PrivateKey privkey, byte[] data, OpenVPNManagement.SignaturePadding padding, String hashalg, String saltlen) throws SignatureException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
+ private byte[] doDigestSign(PrivateKey privkey, byte[] data, OpenVPNManagement.SignaturePadding padding, String hashalg, String saltlen) throws SignatureException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchProviderException {
/* RSA */
Signature sig = null;
- if (privkey.getAlgorithm().equals("EC")) {
+ if (privkey.getAlgorithm().equals("Ed25519")) {
+ sig = Signature.getInstance("Ed25519", "BC");
+ } else if (privkey.getAlgorithm().equals("EC")) {
if (hashalg.equals(""))
hashalg = "NONE";
/* e.g. SHA512withECDSA */
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 ff27a5a2..6a5b016e 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/ConfigParser.java
@@ -5,8 +5,6 @@
package de.blinkt.openvpn.core;
-import static de.blinkt.openvpn.core.connection.Connection.TransportType.PT;
-
import android.os.Build;
import android.text.TextUtils;
@@ -27,9 +25,8 @@ 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.Obfs4HopConnection;
import de.blinkt.openvpn.core.connection.OpenvpnConnection;
-import se.leap.bitmaskclient.pluggableTransports.Obfs4Options;
+import se.leap.bitmaskclient.pluggableTransports.models.Obfs4Options;
//! Openvpn Config FIle Parser, probably not 100% accurate but close enough
@@ -808,12 +805,10 @@ public class ConfigParser {
}
else {
switch (transportType) {
+ case OBFS4_HOP:
case OBFS4:
conn = new Obfs4Connection(obfs4Options);
break;
- case OBFS4_HOP:
- conn = new Obfs4HopConnection(obfs4Options);
- break;
case OPENVPN:
conn = new OpenvpnConnection();
break;
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 506b04a6..a82a87d9 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java
@@ -10,7 +10,6 @@ import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_CONNECTED;
import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_WAITING_FOR_USER_INPUT;
import static de.blinkt.openvpn.core.NetworkSpace.IpAddress;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PROFILE;
-import static se.leap.bitmaskclient.base.utils.BuildConfigHelper.useObfsVpn;
import android.Manifest.permission;
import android.app.Notification;
@@ -53,9 +52,7 @@ import se.leap.bitmaskclient.R;
import se.leap.bitmaskclient.eip.EipStatus;
import se.leap.bitmaskclient.eip.VpnNotificationManager;
import se.leap.bitmaskclient.firewall.FirewallManager;
-import se.leap.bitmaskclient.pluggableTransports.PtClientBuilder;
-import se.leap.bitmaskclient.pluggableTransports.PtClientInterface;
-import se.leap.bitmaskclient.pluggableTransports.ShapeshifterClient;
+import se.leap.bitmaskclient.pluggableTransports.ObfsvpnClient;
public class OpenVPNService extends VpnService implements StateListener, Callback, ByteCountListener, IOpenVPNServiceInternal, VpnNotificationManager.VpnServiceCallback {
@@ -91,8 +88,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
private Toast mlastToast;
private Runnable mOpenVPNThread;
private VpnNotificationManager notificationManager;
- private ShapeshifterClient shapeshifter;
- private PtClientInterface obfsVpnClient;
+ private ObfsvpnClient obfsVpnClient;
private FirewallManager firewallManager;
private final IBinder mBinder = new IOpenVPNServiceInternal.Stub() {
@@ -186,8 +182,10 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
synchronized (mProcessLock) {
mProcessThread = null;
}
+ stopObfsvpn();
VpnStatus.removeByteCountListener(this);
- unregisterDeviceStateReceiver();
+ unregisterDeviceStateReceiver(mDeviceStateReceiver);
+ mDeviceStateReceiver = null;
mOpenVPNThread = null;
if (!mStarting) {
stopForeground(!mNotificationAlwaysVisible);
@@ -200,35 +198,31 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
firewallManager.stop();
}
- synchronized void registerDeviceStateReceiver(OpenVPNManagement magnagement) {
+ synchronized void registerDeviceStateReceiver(DeviceStateReceiver newDeviceStateReceiver) {
// Registers BroadcastReceiver to track network connection changes.
IntentFilter filter = new IntentFilter();
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_SCREEN_ON);
- mDeviceStateReceiver = new DeviceStateReceiver(magnagement);
// Fetch initial network state
- mDeviceStateReceiver.networkStateChange(this);
-
- registerReceiver(mDeviceStateReceiver, filter);
- VpnStatus.addByteCountListener(mDeviceStateReceiver);
+ newDeviceStateReceiver.networkStateChange(this);
+ registerReceiver(newDeviceStateReceiver, filter);
+ VpnStatus.addByteCountListener(newDeviceStateReceiver);
}
- synchronized void unregisterDeviceStateReceiver() {
+ synchronized void unregisterDeviceStateReceiver(DeviceStateReceiver deviceStateReceiver) {
if (mDeviceStateReceiver != null)
try {
- VpnStatus.removeByteCountListener(mDeviceStateReceiver);
- this.unregisterReceiver(mDeviceStateReceiver);
+ VpnStatus.removeByteCountListener(deviceStateReceiver);
+ this.unregisterReceiver(deviceStateReceiver);
} catch (IllegalArgumentException iae) {
// I don't know why this happens:
// java.lang.IllegalArgumentException: Receiver not registered: de.blinkt.openvpn.NetworkSateReceiver@41a61a10
// Ignore for now ...
iae.printStackTrace();
}
- mDeviceStateReceiver = null;
-
}
@Override
@@ -237,18 +231,20 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
mDeviceStateReceiver.userPause(shouldBePaused);
}
+ private boolean stopObfsvpn() {
+ if (obfsVpnClient == null || !obfsVpnClient.isStarted()) {
+ return true;
+ }
+ boolean success = obfsVpnClient.stop();
+ obfsVpnClient = null;
+ return success;
+ }
@Override
public boolean stopVPN(boolean replaceConnection) {
+ stopObfsvpn();
if(isVpnRunning()) {
if (getManagement() != null && getManagement().stopVPN(replaceConnection)) {
if (!replaceConnection) {
- if (shapeshifter != null) {
- shapeshifter.stop();
- shapeshifter = null;
- } else if (obfsVpnClient != null && obfsVpnClient.isStarted()) {
- obfsVpnClient.stop();
- obfsVpnClient = null;
- }
VpnStatus.updateStateString("NOPROCESS", "VPN STOPPED", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED);
}
return true;
@@ -379,24 +375,53 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
}
private void startOpenVPN() {
- //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];
VpnStatus.setCurrentlyConnectingProfile(mProfile);
+ // stop old running obfsvpn client
+ if (!stopObfsvpn()) {
+ VpnStatus.logError("Failed to stop already running obfsvpn client");
+ endVpnService();
+ VpnStatus.updateStateString("NOPROCESS", "VPN STOPPED", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED);
+ return;
+ }
+
+ // Set a flag that we are starting a new VPN
+ mStarting = true;
+ // Stop the previous session by interrupting the thread.
+ stopOldOpenVPNProcess();
+ // An old running VPN should now be exited
+ mStarting = false;
+
+ // optionally start start obfsvpn and adapt openvpn config to the port obfsvpn is listening to
+ Connection.TransportType transportType = connection.getTransportType();
+ if (mProfile.usePluggableTransports() && transportType.isPluggableTransport()) {
+ try {
+ obfsVpnClient = new ObfsvpnClient(((Obfs4Connection) connection).getObfs4Options());
+ obfsVpnClient.start();
+ int port = obfsVpnClient.getPort();
+ connection.setServerPort(String.valueOf(port));
+ } catch (RuntimeException e) {
+ e.printStackTrace();
+ VpnStatus.logException(e);
+ endVpnService();
+ VpnStatus.updateStateString("NOPROCESS", "VPN STOPPED", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED);
+ return;
+ }
+ }
+
+ // write openvpn config
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) {
VpnStatus.logException("Error writing config file", e);
endVpnService();
+ VpnStatus.updateStateString("NOPROCESS", "VPN STOPPED", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED);
return;
}
+
String nativeLibraryDirectory = getApplicationInfo().nativeLibraryDir;
String tmpDir;
try {
@@ -409,31 +434,6 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
// Write OpenVPN binary
String[] argv = VPNLaunchHelper.buildOpenvpnArgv(this);
-
- // Set a flag that we are starting a new VPN
- mStarting = true;
- // Stop the previous session by interrupting the thread.
-
- stopOldOpenVPNProcess();
- // An old running VPN should now be exited
- mStarting = false;
- Connection.TransportType transportType = connection.getTransportType();
- if (mProfile.usePluggableTransports() && transportType.isPluggableTransport()) {
- if (useObfsVpn()) {
- if (obfsVpnClient != null && obfsVpnClient.isStarted()) {
- obfsVpnClient.stop();
- }
- obfsVpnClient = PtClientBuilder.getPtClient(connection);
- int runningSocksPort = obfsVpnClient.start();
- if (connection.getTransportType() == Connection.TransportType.OBFS4) {
- connection.setProxyPort(String.valueOf(runningSocksPort));
- }
- } else if (shapeshifter == null) {
- shapeshifter = new ShapeshifterClient(((Obfs4Connection) connection).getObfs4Options());
- shapeshifter.start();
- }
- }
-
// Start a new session by creating a new thread.
boolean useOpenVPN3 = VpnProfile.doUseOpenVPN3(this);
@@ -469,14 +469,16 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
mProcessThread.start();
}
- new Handler(getMainLooper()).post(() -> {
- if (mDeviceStateReceiver != null) {
- unregisterDeviceStateReceiver();
- }
- registerDeviceStateReceiver(mManagement);
- }
+ final DeviceStateReceiver oldDeviceStateReceiver = mDeviceStateReceiver;
+ final DeviceStateReceiver newDeviceStateReceiver = new DeviceStateReceiver(mManagement);
- );
+ guiHandler.post(() -> {
+ if (oldDeviceStateReceiver != null)
+ unregisterDeviceStateReceiver(oldDeviceStateReceiver);
+
+ registerDeviceStateReceiver(newDeviceStateReceiver);
+ mDeviceStateReceiver = newDeviceStateReceiver;
+ });
}
private void stopOldOpenVPNProcess() {
@@ -485,16 +487,6 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
if (mOpenVPNThread != null)
((OpenVPNThread) mOpenVPNThread).setReplaceConnection();
if (mManagement.stopVPN(true)) {
- // an old was asked to exit, wait 1s
- if (shapeshifter != null) {
- Log.d(TAG, "-> stop shapeshifter");
- shapeshifter.stop();
- shapeshifter = null;
- } else if (obfsVpnClient != null && obfsVpnClient.isStarted()) {
- Log.d(TAG, "-> stop obfsvpnClient");
- obfsVpnClient.stop();
- obfsVpnClient = null;
- }
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
@@ -538,6 +530,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
@Override
public void onCreate() {
super.onCreate();
+ guiHandler = new Handler(getMainLooper());
notificationManager = new VpnNotificationManager(this);
firewallManager = new FirewallManager(this, true);
}
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 88b933eb..a4b5e3be 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java
@@ -272,12 +272,13 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement {
}
private void processCommand(String command) {
- //Log.i(TAG, "Line from managment" + command);
+ Log.i(TAG, "Line from managment " + command);
if (command.startsWith(">") && command.contains(":")) {
String[] parts = command.split(":", 2);
String cmd = parts[0].substring(1);
String argument = parts[1];
+ Log.d(">>>>", "CMD: "+ cmd + "argument: " + argument);
switch (cmd) {
@@ -735,7 +736,7 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement {
String[] arguments = argument.split(",");
// NC9t8IkYrjAQcCzc85zN0H5TvwfAUDwYkR4j2ga6fGw=,RSA_PKCS1_PSS_PADDING,hashalg=SHA256,saltlen=digest
-
+ // ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFRMUyAxLjMsIGNsaWVudCBIoXJ0aWZpY2F0ZVZlcmlmeQCvvTk69HvSHUhM27ghCCSgzHds1Bdsm4MyVGxlgDIJbnDj+G5Y1YxXajqy6E/G1GA=,ED25519,data=message
SignaturePadding padding = SignaturePadding.NO_PADDING;
String saltlen="";
diff --git a/app/src/main/java/de/blinkt/openvpn/core/VpnStatus.java b/app/src/main/java/de/blinkt/openvpn/core/VpnStatus.java
index ecc03a19..73616ba4 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/VpnStatus.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/VpnStatus.java
@@ -223,7 +223,7 @@ public class VpnStatus {
public enum ErrorType {
UNKNOWN,
- SHAPESHIFTER
+ OBFSVPN
}
// keytool -printcert -jarfile de.blinkt.openvpn_85.apk
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
index 0b28cbca..6cd86105 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java
@@ -9,10 +9,13 @@ import static de.blinkt.openvpn.core.connection.Connection.TransportType.*;
import android.text.TextUtils;
+import androidx.annotation.NonNull;
+
import com.google.gson.annotations.JsonAdapter;
import java.io.Serializable;
import java.util.Locale;
+import java.util.Objects;
@JsonAdapter(ConnectionAdapter.class)
public abstract class Connection implements Serializable, Cloneable {
@@ -42,7 +45,8 @@ public abstract class Connection implements Serializable, Cloneable {
public enum TransportProtocol {
UDP("udp"),
TCP("tcp"),
- KCP("kcp");
+ KCP("kcp"),
+ QUIC("quic");
final String protocol;
@@ -301,5 +305,54 @@ public abstract class Connection implements Serializable, Cloneable {
this.mProxyAuthPassword = proxyAuthPassword;
}
- public abstract TransportType getTransportType();
+ public abstract @NonNull TransportType getTransportType();
+
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Connection that)) return false;
+
+ if (mUseUdp != that.mUseUdp) return false;
+ if (mUseCustomConfig != that.mUseCustomConfig) return false;
+ if (mEnabled != that.mEnabled) return false;
+ if (mConnectTimeout != that.mConnectTimeout) return false;
+ if (mUseProxyAuth != that.mUseProxyAuth) return false;
+ if (!Objects.equals(mServerName, that.mServerName))
+ return false;
+ if (!Objects.equals(mServerPort, that.mServerPort))
+ return false;
+ if (!Objects.equals(mCustomConfiguration, that.mCustomConfiguration))
+ return false;
+ if (mProxyType != that.mProxyType) return false;
+ if (!Objects.equals(mProxyName, that.mProxyName))
+ return false;
+ if (!Objects.equals(mProxyPort, that.mProxyPort))
+ return false;
+ if (!Objects.equals(mProxyAuthUser, that.mProxyAuthUser))
+ return false;
+ if (getTransportType() != that.getTransportType()) {
+ return false;
+ }
+ return Objects.equals(mProxyAuthPassword, that.mProxyAuthPassword);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mServerName != null ? mServerName.hashCode() : 0;
+ result = 31 * result + (mServerPort != null ? mServerPort.hashCode() : 0);
+ result = 31 * result + (mUseUdp ? 1 : 0);
+ result = 31 * result + (mCustomConfiguration != null ? mCustomConfiguration.hashCode() : 0);
+ result = 31 * result + (mUseCustomConfig ? 1 : 0);
+ result = 31 * result + (mEnabled ? 1 : 0);
+ result = 31 * result + mConnectTimeout;
+ result = 31 * result + (mProxyType != null ? mProxyType.hashCode() : 0);
+ result = 31 * result + (mProxyName != null ? mProxyName.hashCode() : 0);
+ result = 31 * result + (mProxyPort != null ? mProxyPort.hashCode() : 0);
+ result = 31 * result + (mUseProxyAuth ? 1 : 0);
+ result = 31 * result + (mProxyAuthUser != null ? mProxyAuthUser.hashCode() : 0);
+ result = 31 * result + (mProxyAuthPassword != null ? mProxyAuthPassword.hashCode() : 0);
+ result = 31 * result + getTransportType().toInt();
+ return result;
+ }
}
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
index 19ea180d..e2c596ac 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java
@@ -1,12 +1,7 @@
package de.blinkt.openvpn.core.connection;
-import static se.leap.bitmaskclient.base.utils.BuildConfigHelper.useObfsVpn;
-import static se.leap.bitmaskclient.pluggableTransports.ShapeshifterClient.DISPATCHER_IP;
-import static se.leap.bitmaskclient.pluggableTransports.ShapeshifterClient.DISPATCHER_PORT;
-
-import se.leap.bitmaskclient.pluggableTransports.HoppingObfsVpnClient;
-import se.leap.bitmaskclient.pluggableTransports.Obfs4Options;
-import se.leap.bitmaskclient.pluggableTransports.ObfsVpnClient;
+import se.leap.bitmaskclient.pluggableTransports.ObfsvpnClient;
+import se.leap.bitmaskclient.pluggableTransports.models.Obfs4Options;
/**
@@ -19,33 +14,12 @@ public class Obfs4Connection extends Connection {
private Obfs4Options options;
public Obfs4Connection(Obfs4Options options) {
- if (useObfsVpn()) {
- setServerName(options.gatewayIP);
- setServerPort(options.transport.getPorts()[0]);
- setProxyName(ObfsVpnClient.SOCKS_IP);
- setProxyType(ProxyType.SOCKS5);
- switch (options.transport.getTransportType()) {
- case OBFS4:
- setUseUdp(false);
- setProxyPort(String.valueOf(ObfsVpnClient.SOCKS_PORT.get()));
- break;
- case OBFS4_HOP:
- setUseUdp(true);
- setProxyPort(String.valueOf(HoppingObfsVpnClient.PORT));
- break;
- default:break;
- }
- } else {
- setServerName(DISPATCHER_IP);
- setServerPort(DISPATCHER_PORT);
- setProxyName("");
- setProxyPort("");
- setProxyType(ProxyType.NONE);
-
- // while udp/kcp might be used on the wire,
- // we don't use udp for openvpn in case of a obfs4 connection
- setUseUdp(false);
- }
+ setServerName(ObfsvpnClient.IP);
+ setServerPort(String.valueOf(ObfsvpnClient.DEFAULT_PORT));
+ setUseUdp(true);
+ setProxyType(ProxyType.NONE);
+ setProxyName("");
+ setProxyPort("");
setProxyAuthUser(null);
setProxyAuthPassword(null);
setUseProxyAuth(false);
@@ -61,7 +35,7 @@ public class Obfs4Connection extends Connection {
@Override
public TransportType getTransportType() {
- return TransportType.OBFS4;
+ return options.transport.getTransportType();
}
diff --git a/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4HopConnection.java b/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4HopConnection.java
deleted file mode 100644
index f983ae20..00000000
--- a/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4HopConnection.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package de.blinkt.openvpn.core.connection;
-
-import se.leap.bitmaskclient.pluggableTransports.HoppingObfsVpnClient;
-import se.leap.bitmaskclient.pluggableTransports.Obfs4Options;
-
-
-/**
- * Created by cyberta on 08.03.19.
- */
-
-public class Obfs4HopConnection extends Connection {
-
- private static final String TAG = Obfs4HopConnection.class.getName();
- private Obfs4Options options;
-
- public Obfs4HopConnection(Obfs4Options options) {
- setServerName(HoppingObfsVpnClient.IP);
- setServerPort(String.valueOf(HoppingObfsVpnClient.PORT));
- setProxyName("");
- setProxyPort("");
- setProxyType(ProxyType.NONE);
-
-
- setUseUdp(true);
- setProxyAuthUser(null);
- setProxyAuthPassword(null);
- setUseProxyAuth(false);
- this.options = options;
- }
-
- @Override
- public Connection clone() throws CloneNotSupportedException {
- Obfs4HopConnection connection = (Obfs4HopConnection) super.clone();
- connection.options = this.options;
- return connection;
- }
-
- @Override
- public TransportType getTransportType() {
- return TransportType.OBFS4_HOP;
- }
-
-
- public Obfs4Options getObfs4Options() {
- return options;
- }
-
-}
diff --git a/app/src/main/java/io/swagger/client/JSON.java b/app/src/main/java/io/swagger/client/JSON.java
new file mode 100644
index 00000000..b1ca69a8
--- /dev/null
+++ b/app/src/main/java/io/swagger/client/JSON.java
@@ -0,0 +1,399 @@
+/*
+ * Menshen API
+ * This is a LEAP VPN Service API
+ *
+ * OpenAPI spec version: 0.5.2
+ *
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+
+
+package io.swagger.client;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.internal.bind.util.ISO8601Utils;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import com.google.gson.JsonElement;
+import io.gsonfire.GsonFireBuilder;
+import io.gsonfire.TypeSelector;
+import org.threeten.bp.LocalDate;
+import org.threeten.bp.OffsetDateTime;
+import org.threeten.bp.format.DateTimeFormatter;
+
+import io.swagger.client.model.*;
+import io.swagger.client.model.*;
+import io.swagger.client.model.*;
+import io.swagger.client.model.*;
+import io.swagger.client.model.*;
+import io.swagger.client.model.*;
+import okio.ByteString;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.reflect.Type;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.util.Date;
+import java.util.Map;
+import java.util.HashMap;
+
+public class JSON {
+ private Gson gson;
+ private boolean isLenientOnJson = false;
+ private DateTypeAdapter dateTypeAdapter = new DateTypeAdapter();
+ private SqlDateTypeAdapter sqlDateTypeAdapter = new SqlDateTypeAdapter();
+ private OffsetDateTimeTypeAdapter offsetDateTimeTypeAdapter = new OffsetDateTimeTypeAdapter();
+ private LocalDateTypeAdapter localDateTypeAdapter = new LocalDateTypeAdapter();
+ private ByteArrayAdapter byteArrayAdapter = new ByteArrayAdapter();
+
+ public static GsonBuilder createGson() {
+ GsonFireBuilder fireBuilder = new GsonFireBuilder()
+ ;
+ GsonBuilder builder = fireBuilder.createGsonBuilder();
+ return builder;
+ }
+
+ private static String getDiscriminatorValue(JsonElement readElement, String discriminatorField) {
+ JsonElement element = readElement.getAsJsonObject().get(discriminatorField);
+ if(null == element) {
+ throw new IllegalArgumentException("missing discriminator field: <" + discriminatorField + ">");
+ }
+ return element.getAsString();
+ }
+
+ private static Class getClassByDiscriminator(Map classByDiscriminatorValue, String discriminatorValue) {
+ Class clazz = (Class) classByDiscriminatorValue.get(discriminatorValue.toUpperCase());
+ if(null == clazz) {
+ throw new IllegalArgumentException("cannot determine model class of name: <" + discriminatorValue + ">");
+ }
+ return clazz;
+ }
+
+ public JSON() {
+ gson = createGson()
+ .registerTypeAdapter(Date.class, dateTypeAdapter)
+ .registerTypeAdapter(java.sql.Date.class, sqlDateTypeAdapter)
+ .registerTypeAdapter(OffsetDateTime.class, offsetDateTimeTypeAdapter)
+ .registerTypeAdapter(LocalDate.class, localDateTypeAdapter)
+ .registerTypeAdapter(byte[].class, byteArrayAdapter)
+ .create();
+ }
+
+ /**
+ * Get Gson.
+ *
+ * @return Gson
+ */
+ public Gson getGson() {
+ return gson;
+ }
+
+ /**
+ * Set Gson.
+ *
+ * @param gson Gson
+ * @return JSON
+ */
+ public JSON setGson(Gson gson) {
+ this.gson = gson;
+ return this;
+ }
+
+ public JSON setLenientOnJson(boolean lenientOnJson) {
+ isLenientOnJson = lenientOnJson;
+ return this;
+ }
+
+ /**
+ * Serialize the given Java object into JSON string.
+ *
+ * @param obj Object
+ * @return String representation of the JSON
+ */
+ public String serialize(Object obj) {
+ return gson.toJson(obj);
+ }
+
+ /**
+ * Deserialize the given JSON string to Java object.
+ *
+ * @param <T> Type
+ * @param body The JSON string
+ * @param returnType The type to deserialize into
+ * @return The deserialized Java object
+ */
+ @SuppressWarnings("unchecked")
+ public <T> T deserialize(String body, Type returnType) {
+ try {
+ if (isLenientOnJson) {
+ JsonReader jsonReader = new JsonReader(new StringReader(body));
+ // see https://google-gson.googlecode.com/svn/trunk/gson/docs/javadocs/com/google/gson/stream/JsonReader.html#setLenient(boolean)
+ jsonReader.setLenient(true);
+ return gson.fromJson(jsonReader, returnType);
+ } else {
+ return gson.fromJson(body, returnType);
+ }
+ } catch (JsonParseException e) {
+ // Fallback processing when failed to parse JSON form response body:
+ // return the response body string directly for the String return type;
+ if (returnType.equals(String.class))
+ return (T) body;
+ else throw (e);
+ }
+ }
+
+ /**
+ * Gson TypeAdapter for Byte Array type
+ */
+ public class ByteArrayAdapter extends TypeAdapter<byte[]> {
+
+ @Override
+ public void write(JsonWriter out, byte[] value) throws IOException {
+ if (value == null) {
+ out.nullValue();
+ } else {
+ out.value(ByteString.of(value).base64());
+ }
+ }
+
+ @Override
+ public byte[] read(JsonReader in) throws IOException {
+ switch (in.peek()) {
+ case NULL:
+ in.nextNull();
+ return null;
+ default:
+ String bytesAsBase64 = in.nextString();
+ ByteString byteString = ByteString.decodeBase64(bytesAsBase64);
+ return byteString.toByteArray();
+ }
+ }
+ }
+
+ /**
+ * Gson TypeAdapter for JSR310 OffsetDateTime type
+ */
+ public static class OffsetDateTimeTypeAdapter extends TypeAdapter<OffsetDateTime> {
+
+ private DateTimeFormatter formatter;
+
+ public OffsetDateTimeTypeAdapter() {
+ this(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+ }
+
+ public OffsetDateTimeTypeAdapter(DateTimeFormatter formatter) {
+ this.formatter = formatter;
+ }
+
+ public void setFormat(DateTimeFormatter dateFormat) {
+ this.formatter = dateFormat;
+ }
+
+ @Override
+ public void write(JsonWriter out, OffsetDateTime date) throws IOException {
+ if (date == null) {
+ out.nullValue();
+ } else {
+ out.value(formatter.format(date));
+ }
+ }
+
+ @Override
+ public OffsetDateTime read(JsonReader in) throws IOException {
+ switch (in.peek()) {
+ case NULL:
+ in.nextNull();
+ return null;
+ default:
+ String date = in.nextString();
+ if (date.endsWith("+0000")) {
+ date = date.substring(0, date.length()-5) + "Z";
+ }
+ return OffsetDateTime.parse(date, formatter);
+ }
+ }
+ }
+
+ /**
+ * Gson TypeAdapter for JSR310 LocalDate type
+ */
+ public class LocalDateTypeAdapter extends TypeAdapter<LocalDate> {
+
+ private DateTimeFormatter formatter;
+
+ public LocalDateTypeAdapter() {
+ this(DateTimeFormatter.ISO_LOCAL_DATE);
+ }
+
+ public LocalDateTypeAdapter(DateTimeFormatter formatter) {
+ this.formatter = formatter;
+ }
+
+ public void setFormat(DateTimeFormatter dateFormat) {
+ this.formatter = dateFormat;
+ }
+
+ @Override
+ public void write(JsonWriter out, LocalDate date) throws IOException {
+ if (date == null) {
+ out.nullValue();
+ } else {
+ out.value(formatter.format(date));
+ }
+ }
+
+ @Override
+ public LocalDate read(JsonReader in) throws IOException {
+ switch (in.peek()) {
+ case NULL:
+ in.nextNull();
+ return null;
+ default:
+ String date = in.nextString();
+ return LocalDate.parse(date, formatter);
+ }
+ }
+ }
+
+ public JSON setOffsetDateTimeFormat(DateTimeFormatter dateFormat) {
+ offsetDateTimeTypeAdapter.setFormat(dateFormat);
+ return this;
+ }
+
+ public JSON setLocalDateFormat(DateTimeFormatter dateFormat) {
+ localDateTypeAdapter.setFormat(dateFormat);
+ return this;
+ }
+
+ /**
+ * Gson TypeAdapter for java.sql.Date type
+ * If the dateFormat is null, a simple "yyyy-MM-dd" format will be used
+ * (more efficient than SimpleDateFormat).
+ */
+ public static class SqlDateTypeAdapter extends TypeAdapter<java.sql.Date> {
+
+ private DateFormat dateFormat;
+
+ public SqlDateTypeAdapter() {
+ }
+
+ public SqlDateTypeAdapter(DateFormat dateFormat) {
+ this.dateFormat = dateFormat;
+ }
+
+ public void setFormat(DateFormat dateFormat) {
+ this.dateFormat = dateFormat;
+ }
+
+ @Override
+ public void write(JsonWriter out, java.sql.Date date) throws IOException {
+ if (date == null) {
+ out.nullValue();
+ } else {
+ String value;
+ if (dateFormat != null) {
+ value = dateFormat.format(date);
+ } else {
+ value = date.toString();
+ }
+ out.value(value);
+ }
+ }
+
+ @Override
+ public java.sql.Date read(JsonReader in) throws IOException {
+ switch (in.peek()) {
+ case NULL:
+ in.nextNull();
+ return null;
+ default:
+ String date = in.nextString();
+ try {
+ if (dateFormat != null) {
+ return new java.sql.Date(dateFormat.parse(date).getTime());
+ }
+ return new java.sql.Date(ISO8601Utils.parse(date, new ParsePosition(0)).getTime());
+ } catch (ParseException e) {
+ throw new JsonParseException(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Gson TypeAdapter for java.util.Date type
+ * If the dateFormat is null, ISO8601Utils will be used.
+ */
+ public static class DateTypeAdapter extends TypeAdapter<Date> {
+
+ private DateFormat dateFormat;
+
+ public DateTypeAdapter() {
+ }
+
+ public DateTypeAdapter(DateFormat dateFormat) {
+ this.dateFormat = dateFormat;
+ }
+
+ public void setFormat(DateFormat dateFormat) {
+ this.dateFormat = dateFormat;
+ }
+
+ @Override
+ public void write(JsonWriter out, Date date) throws IOException {
+ if (date == null) {
+ out.nullValue();
+ } else {
+ String value;
+ if (dateFormat != null) {
+ value = dateFormat.format(date);
+ } else {
+ value = ISO8601Utils.format(date, true);
+ }
+ out.value(value);
+ }
+ }
+
+ @Override
+ public Date read(JsonReader in) throws IOException {
+ try {
+ switch (in.peek()) {
+ case NULL:
+ in.nextNull();
+ return null;
+ default:
+ String date = in.nextString();
+ try {
+ if (dateFormat != null) {
+ return dateFormat.parse(date);
+ }
+ return ISO8601Utils.parse(date, new ParsePosition(0));
+ } catch (ParseException e) {
+ throw new JsonParseException(e);
+ }
+ }
+ } catch (IllegalArgumentException e) {
+ throw new JsonParseException(e);
+ }
+ }
+ }
+
+ public JSON setDateFormat(DateFormat dateFormat) {
+ dateTypeAdapter.setFormat(dateFormat);
+ return this;
+ }
+
+ public JSON setSqlDateFormat(DateFormat dateFormat) {
+ sqlDateTypeAdapter.setFormat(dateFormat);
+ return this;
+ }
+
+}
diff --git a/app/src/main/java/io/swagger/client/model/ModelsBridge.java b/app/src/main/java/io/swagger/client/model/ModelsBridge.java
new file mode 100644
index 00000000..88f3575e
--- /dev/null
+++ b/app/src/main/java/io/swagger/client/model/ModelsBridge.java
@@ -0,0 +1,428 @@
+/*
+ * Menshen API
+ * This is a LEAP VPN Service API
+ *
+ * OpenAPI spec version: 0.5.2
+ *
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+
+
+package io.swagger.client.model;
+
+import java.util.Objects;
+import java.util.Arrays;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * ModelsBridge
+ */
+
+public class ModelsBridge {
+ @SerializedName("auth")
+ private String auth = null;
+
+ @SerializedName("bucket")
+ private String bucket = null;
+
+ @SerializedName("experimental")
+ private Boolean experimental = null;
+
+ @SerializedName("healthy")
+ private Boolean healthy = null;
+
+ @SerializedName("host")
+ private String host = null;
+
+ @SerializedName("ip_addr")
+ private String ipAddr = null;
+
+ @SerializedName("ip6_addr")
+ private String ip6Addr = null;
+
+ @SerializedName("last_seen_millis")
+ private Long lastSeenMillis = null;
+
+ @SerializedName("load")
+ private BigDecimal load = null;
+
+ @SerializedName("location")
+ private String location = null;
+
+ @SerializedName("options")
+ private Map<String, Object> options = null;
+
+ @SerializedName("overloaded")
+ private Boolean overloaded = null;
+
+ @SerializedName("port")
+ private Integer port = null;
+
+ @SerializedName("transport")
+ private String transport = null;
+
+ @SerializedName("type")
+ private String type = null;
+
+ public ModelsBridge auth(String auth) {
+ this.auth = auth;
+ return this;
+ }
+
+ /**
+ * Any authentication method needed for connect to the bridge, &#x60;none&#x60; otherwise.
+ * @return auth
+ **/
+ @ApiModelProperty(value = "Any authentication method needed for connect to the bridge, `none` otherwise.")
+ public String getAuth() {
+ return auth;
+ }
+
+ public void setAuth(String auth) {
+ this.auth = auth;
+ }
+
+ public ModelsBridge bucket(String bucket) {
+ this.bucket = bucket;
+ return this;
+ }
+
+ /**
+ * Bucket is a \&quot;bucket\&quot; tag that connotes a resource group that a user may or may not have access to. An empty bucket string implies that it is open access
+ * @return bucket
+ **/
+ @ApiModelProperty(value = "Bucket is a \"bucket\" tag that connotes a resource group that a user may or may not have access to. An empty bucket string implies that it is open access")
+ public String getBucket() {
+ return bucket;
+ }
+
+ public void setBucket(String bucket) {
+ this.bucket = bucket;
+ }
+
+ public ModelsBridge experimental(Boolean experimental) {
+ this.experimental = experimental;
+ return this;
+ }
+
+ /**
+ * An experimental bridge flags any bridge that, for whatever reason, is not deemed stable. The expectation is that clients have to opt-in to experimental bridges (and gateways too).
+ * @return experimental
+ **/
+ @ApiModelProperty(value = "An experimental bridge flags any bridge that, for whatever reason, is not deemed stable. The expectation is that clients have to opt-in to experimental bridges (and gateways too).")
+ public Boolean isExperimental() {
+ return experimental;
+ }
+
+ public void setExperimental(Boolean experimental) {
+ this.experimental = experimental;
+ }
+
+ public ModelsBridge healthy(Boolean healthy) {
+ this.healthy = healthy;
+ return this;
+ }
+
+ /**
+ * Healthy indicates whether this bridge can be used normally.
+ * @return healthy
+ **/
+ @ApiModelProperty(value = "Healthy indicates whether this bridge can be used normally.")
+ public Boolean isHealthy() {
+ return healthy;
+ }
+
+ public void setHealthy(Boolean healthy) {
+ this.healthy = healthy;
+ }
+
+ public ModelsBridge host(String host) {
+ this.host = host;
+ return this;
+ }
+
+ /**
+ * Host is a unique identifier for the bridge.
+ * @return host
+ **/
+ @ApiModelProperty(value = "Host is a unique identifier for the bridge.")
+ public String getHost() {
+ return host;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ public ModelsBridge ipAddr(String ipAddr) {
+ this.ipAddr = ipAddr;
+ return this;
+ }
+
+ /**
+ * IPAddr is the IPv4 address
+ * @return ipAddr
+ **/
+ @ApiModelProperty(value = "IPAddr is the IPv4 address")
+ public String getIpAddr() {
+ return ipAddr;
+ }
+
+ public void setIpAddr(String ipAddr) {
+ this.ipAddr = ipAddr;
+ }
+
+ public ModelsBridge ip6Addr(String ip6Addr) {
+ this.ip6Addr = ip6Addr;
+ return this;
+ }
+
+ /**
+ * IP6Addr is the IPv6 address
+ * @return ip6Addr
+ **/
+ @ApiModelProperty(value = "IP6Addr is the IPv6 address")
+ public String getIp6Addr() {
+ return ip6Addr;
+ }
+
+ public void setIp6Addr(String ip6Addr) {
+ this.ip6Addr = ip6Addr;
+ }
+
+ public ModelsBridge lastSeenMillis(Long lastSeenMillis) {
+ this.lastSeenMillis = lastSeenMillis;
+ return this;
+ }
+
+ /**
+ * LastSeenMillis is a unix time in milliseconds representing the last time we received a heartbeat update from this bridge
+ * @return lastSeenMillis
+ **/
+ @ApiModelProperty(value = "LastSeenMillis is a unix time in milliseconds representing the last time we received a heartbeat update from this bridge")
+ public Long getLastSeenMillis() {
+ return lastSeenMillis;
+ }
+
+ public void setLastSeenMillis(Long lastSeenMillis) {
+ this.lastSeenMillis = lastSeenMillis;
+ }
+
+ public ModelsBridge load(BigDecimal load) {
+ this.load = load;
+ return this;
+ }
+
+ /**
+ * Load is the fractional load - but for now menshen agent is not measuring load in the bridges.
+ * @return load
+ **/
+ @ApiModelProperty(value = "Load is the fractional load - but for now menshen agent is not measuring load in the bridges.")
+ public BigDecimal getLoad() {
+ return load;
+ }
+
+ public void setLoad(BigDecimal load) {
+ this.load = load;
+ }
+
+ public ModelsBridge location(String location) {
+ this.location = location;
+ return this;
+ }
+
+ /**
+ * Location refers to the location to which this bridge points to
+ * @return location
+ **/
+ @ApiModelProperty(value = "Location refers to the location to which this bridge points to")
+ public String getLocation() {
+ return location;
+ }
+
+ public void setLocation(String location) {
+ this.location = location;
+ }
+
+ public ModelsBridge options(Map<String, Object> options) {
+ this.options = options;
+ return this;
+ }
+
+ public ModelsBridge putOptionsItem(String key, Object optionsItem) {
+ if (this.options == null) {
+ this.options = new HashMap<String, Object>();
+ }
+ this.options.put(key, optionsItem);
+ return this;
+ }
+
+ /**
+ * Options contain the map of options that will be passed to the client. It usually contains authentication credentials.
+ * @return options
+ **/
+ @ApiModelProperty(value = "Options contain the map of options that will be passed to the client. It usually contains authentication credentials.")
+ public Map<String, Object> getOptions() {
+ return options;
+ }
+
+ public void setOptions(Map<String, Object> options) {
+ this.options = options;
+ }
+
+ public ModelsBridge overloaded(Boolean overloaded) {
+ this.overloaded = overloaded;
+ return this;
+ }
+
+ /**
+ * Overloaded should be set to true if the fractional load is above threshold.
+ * @return overloaded
+ **/
+ @ApiModelProperty(value = "Overloaded should be set to true if the fractional load is above threshold.")
+ public Boolean isOverloaded() {
+ return overloaded;
+ }
+
+ public void setOverloaded(Boolean overloaded) {
+ this.overloaded = overloaded;
+ }
+
+ public ModelsBridge port(Integer port) {
+ this.port = port;
+ return this;
+ }
+
+ /**
+ * For some protocols (like hopping) port is undefined.
+ * @return port
+ **/
+ @ApiModelProperty(value = "For some protocols (like hopping) port is undefined.")
+ public Integer getPort() {
+ return port;
+ }
+
+ public void setPort(Integer port) {
+ this.port = port;
+ }
+
+ public ModelsBridge transport(String transport) {
+ this.transport = transport;
+ return this;
+ }
+
+ /**
+ * TCP, UDP or KCP. This was called \&quot;protocol\&quot; before.
+ * @return transport
+ **/
+ @ApiModelProperty(value = "TCP, UDP or KCP. This was called \"protocol\" before.")
+ public String getTransport() {
+ return transport;
+ }
+
+ public void setTransport(String transport) {
+ this.transport = transport;
+ }
+
+ public ModelsBridge type(String type) {
+ this.type = type;
+ return this;
+ }
+
+ /**
+ * Type of bridge.
+ * @return type
+ **/
+ @ApiModelProperty(value = "Type of bridge.")
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+
+ @Override
+ public boolean equals(java.lang.Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ModelsBridge modelsBridge = (ModelsBridge) o;
+ return Objects.equals(this.auth, modelsBridge.auth) &&
+ Objects.equals(this.bucket, modelsBridge.bucket) &&
+ Objects.equals(this.experimental, modelsBridge.experimental) &&
+ Objects.equals(this.healthy, modelsBridge.healthy) &&
+ Objects.equals(this.host, modelsBridge.host) &&
+ Objects.equals(this.ipAddr, modelsBridge.ipAddr) &&
+ Objects.equals(this.ip6Addr, modelsBridge.ip6Addr) &&
+ Objects.equals(this.lastSeenMillis, modelsBridge.lastSeenMillis) &&
+ Objects.equals(this.load, modelsBridge.load) &&
+ Objects.equals(this.location, modelsBridge.location) &&
+ Objects.equals(this.options, modelsBridge.options) &&
+ Objects.equals(this.overloaded, modelsBridge.overloaded) &&
+ Objects.equals(this.port, modelsBridge.port) &&
+ Objects.equals(this.transport, modelsBridge.transport) &&
+ Objects.equals(this.type, modelsBridge.type);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(auth, bucket, experimental, healthy, host, ipAddr, ip6Addr, lastSeenMillis, load, location, options, overloaded, port, transport, type);
+ }
+
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class ModelsBridge {\n");
+
+ sb.append(" auth: ").append(toIndentedString(auth)).append("\n");
+ sb.append(" bucket: ").append(toIndentedString(bucket)).append("\n");
+ sb.append(" experimental: ").append(toIndentedString(experimental)).append("\n");
+ sb.append(" healthy: ").append(toIndentedString(healthy)).append("\n");
+ sb.append(" host: ").append(toIndentedString(host)).append("\n");
+ sb.append(" ipAddr: ").append(toIndentedString(ipAddr)).append("\n");
+ sb.append(" ip6Addr: ").append(toIndentedString(ip6Addr)).append("\n");
+ sb.append(" lastSeenMillis: ").append(toIndentedString(lastSeenMillis)).append("\n");
+ sb.append(" load: ").append(toIndentedString(load)).append("\n");
+ sb.append(" location: ").append(toIndentedString(location)).append("\n");
+ sb.append(" options: ").append(toIndentedString(options)).append("\n");
+ sb.append(" overloaded: ").append(toIndentedString(overloaded)).append("\n");
+ sb.append(" port: ").append(toIndentedString(port)).append("\n");
+ sb.append(" transport: ").append(toIndentedString(transport)).append("\n");
+ sb.append(" type: ").append(toIndentedString(type)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(java.lang.Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+
+}
+
diff --git a/app/src/main/java/io/swagger/client/model/ModelsEIPService.java b/app/src/main/java/io/swagger/client/model/ModelsEIPService.java
new file mode 100644
index 00000000..939f8aa6
--- /dev/null
+++ b/app/src/main/java/io/swagger/client/model/ModelsEIPService.java
@@ -0,0 +1,206 @@
+/*
+ * Menshen API
+ * This is a LEAP VPN Service API
+ *
+ * OpenAPI spec version: 0.5.2
+ *
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+
+
+package io.swagger.client.model;
+
+import java.util.Objects;
+import java.util.Arrays;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import io.swagger.client.model.ModelsLocation;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * ModelsEIPService
+ */
+
+public class ModelsEIPService {
+ @SerializedName("auth")
+ private String auth = null;
+
+ @SerializedName("locations")
+ private Map<String, ModelsLocation> locations = null;
+
+ @SerializedName("openvpn_configuration")
+ private Map<String, Object> openvpnConfiguration = null;
+
+ @SerializedName("serial")
+ private Integer serial = null;
+
+ @SerializedName("version")
+ private Integer version = null;
+
+ public ModelsEIPService auth(String auth) {
+ this.auth = auth;
+ return this;
+ }
+
+ /**
+ * Get auth
+ * @return auth
+ **/
+ @ApiModelProperty(value = "")
+ public String getAuth() {
+ return auth;
+ }
+
+ public void setAuth(String auth) {
+ this.auth = auth;
+ }
+
+ public ModelsEIPService locations(Map<String, ModelsLocation> locations) {
+ this.locations = locations;
+ return this;
+ }
+
+ public ModelsEIPService putLocationsItem(String key, ModelsLocation locationsItem) {
+ if (this.locations == null) {
+ this.locations = new HashMap<String, ModelsLocation>();
+ }
+ this.locations.put(key, locationsItem);
+ return this;
+ }
+
+ /**
+ * Get locations
+ * @return locations
+ **/
+ @ApiModelProperty(value = "")
+ public Map<String, ModelsLocation> getLocations() {
+ return locations;
+ }
+
+ public void setLocations(Map<String, ModelsLocation> locations) {
+ this.locations = locations;
+ }
+
+ public ModelsEIPService openvpnConfiguration(Map<String, Object> openvpnConfiguration) {
+ this.openvpnConfiguration = openvpnConfiguration;
+ return this;
+ }
+
+ public ModelsEIPService putOpenvpnConfigurationItem(String key, Object openvpnConfigurationItem) {
+ if (this.openvpnConfiguration == null) {
+ this.openvpnConfiguration = new HashMap<String, Object>();
+ }
+ this.openvpnConfiguration.put(key, openvpnConfigurationItem);
+ return this;
+ }
+
+ /**
+ * Get openvpnConfiguration
+ * @return openvpnConfiguration
+ **/
+ @ApiModelProperty(value = "")
+ public Map<String, Object> getOpenvpnConfiguration() {
+ return openvpnConfiguration;
+ }
+
+ public void setOpenvpnConfiguration(Map<String, Object> openvpnConfiguration) {
+ this.openvpnConfiguration = openvpnConfiguration;
+ }
+
+ public ModelsEIPService serial(Integer serial) {
+ this.serial = serial;
+ return this;
+ }
+
+ /**
+ * Get serial
+ * @return serial
+ **/
+ @ApiModelProperty(value = "")
+ public Integer getSerial() {
+ return serial;
+ }
+
+ public void setSerial(Integer serial) {
+ this.serial = serial;
+ }
+
+ public ModelsEIPService version(Integer version) {
+ this.version = version;
+ return this;
+ }
+
+ /**
+ * Get version
+ * @return version
+ **/
+ @ApiModelProperty(value = "")
+ public Integer getVersion() {
+ return version;
+ }
+
+ public void setVersion(Integer version) {
+ this.version = version;
+ }
+
+
+ @Override
+ public boolean equals(java.lang.Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ModelsEIPService modelsEIPService = (ModelsEIPService) o;
+ return Objects.equals(this.auth, modelsEIPService.auth) &&
+ Objects.equals(this.locations, modelsEIPService.locations) &&
+ Objects.equals(this.openvpnConfiguration, modelsEIPService.openvpnConfiguration) &&
+ Objects.equals(this.serial, modelsEIPService.serial) &&
+ Objects.equals(this.version, modelsEIPService.version);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(auth, locations, openvpnConfiguration, serial, version);
+ }
+
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class ModelsEIPService {\n");
+
+ sb.append(" auth: ").append(toIndentedString(auth)).append("\n");
+ sb.append(" locations: ").append(toIndentedString(locations)).append("\n");
+ sb.append(" openvpnConfiguration: ").append(toIndentedString(openvpnConfiguration)).append("\n");
+ sb.append(" serial: ").append(toIndentedString(serial)).append("\n");
+ sb.append(" version: ").append(toIndentedString(version)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(java.lang.Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+
+}
+
diff --git a/app/src/main/java/io/swagger/client/model/ModelsGateway.java b/app/src/main/java/io/swagger/client/model/ModelsGateway.java
new file mode 100644
index 00000000..8c020eb1
--- /dev/null
+++ b/app/src/main/java/io/swagger/client/model/ModelsGateway.java
@@ -0,0 +1,371 @@
+/*
+ * Menshen API
+ * This is a LEAP VPN Service API
+ *
+ * OpenAPI spec version: 0.5.2
+ *
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+
+
+package io.swagger.client.model;
+
+import java.util.Objects;
+import java.util.Arrays;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import java.io.IOException;
+import java.math.BigDecimal;
+
+/**
+ * ModelsGateway
+ */
+
+public class ModelsGateway {
+ @SerializedName("bucket")
+ private String bucket = null;
+
+ @SerializedName("experimental")
+ private Boolean experimental = null;
+
+ @SerializedName("healthy")
+ private Boolean healthy = null;
+
+ @SerializedName("host")
+ private String host = null;
+
+ @SerializedName("ip_addr")
+ private String ipAddr = null;
+
+ @SerializedName("ip6_addr")
+ private String ip6Addr = null;
+
+ @SerializedName("last_seen_millis")
+ private Long lastSeenMillis = null;
+
+ @SerializedName("load")
+ private BigDecimal load = null;
+
+ @SerializedName("location")
+ private String location = null;
+
+ @SerializedName("overloaded")
+ private Boolean overloaded = null;
+
+ @SerializedName("port")
+ private Integer port = null;
+
+ @SerializedName("transport")
+ private String transport = null;
+
+ @SerializedName("type")
+ private String type = null;
+
+ public ModelsGateway bucket(String bucket) {
+ this.bucket = bucket;
+ return this;
+ }
+
+ /**
+ * Bucket is a \&quot;bucket\&quot; tag that connotes a resource group that a user may or may not have access to. An empty bucket string implies that it is open access
+ * @return bucket
+ **/
+ @ApiModelProperty(value = "Bucket is a \"bucket\" tag that connotes a resource group that a user may or may not have access to. An empty bucket string implies that it is open access")
+ public String getBucket() {
+ return bucket;
+ }
+
+ public void setBucket(String bucket) {
+ this.bucket = bucket;
+ }
+
+ public ModelsGateway experimental(Boolean experimental) {
+ this.experimental = experimental;
+ return this;
+ }
+
+ /**
+ * An experimental gateway flags any gateway that, for whatever reason, is not deemed stable. The expectation is that clients have to opt-in to experimental gateways (and bridges too).
+ * @return experimental
+ **/
+ @ApiModelProperty(value = "An experimental gateway flags any gateway that, for whatever reason, is not deemed stable. The expectation is that clients have to opt-in to experimental gateways (and bridges too).")
+ public Boolean isExperimental() {
+ return experimental;
+ }
+
+ public void setExperimental(Boolean experimental) {
+ this.experimental = experimental;
+ }
+
+ public ModelsGateway healthy(Boolean healthy) {
+ this.healthy = healthy;
+ return this;
+ }
+
+ /**
+ * Not used now - we could potentially flag gateways that are planned to undergo maintenance mode some time in advance. We can also automatically flag as not healthy gateways that appear not to be routing to the internet.
+ * @return healthy
+ **/
+ @ApiModelProperty(value = "Not used now - we could potentially flag gateways that are planned to undergo maintenance mode some time in advance. We can also automatically flag as not healthy gateways that appear not to be routing to the internet.")
+ public Boolean isHealthy() {
+ return healthy;
+ }
+
+ public void setHealthy(Boolean healthy) {
+ this.healthy = healthy;
+ }
+
+ public ModelsGateway host(String host) {
+ this.host = host;
+ return this;
+ }
+
+ /**
+ * Host is a unique identifier for the gateway host. It does not need to resolve, since we&#39;re not using DNS to resolve the gateways.
+ * @return host
+ **/
+ @ApiModelProperty(value = "Host is a unique identifier for the gateway host. It does not need to resolve, since we're not using DNS to resolve the gateways.")
+ public String getHost() {
+ return host;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ public ModelsGateway ipAddr(String ipAddr) {
+ this.ipAddr = ipAddr;
+ return this;
+ }
+
+ /**
+ * IPAddr is the IPv4 address
+ * @return ipAddr
+ **/
+ @ApiModelProperty(value = "IPAddr is the IPv4 address")
+ public String getIpAddr() {
+ return ipAddr;
+ }
+
+ public void setIpAddr(String ipAddr) {
+ this.ipAddr = ipAddr;
+ }
+
+ public ModelsGateway ip6Addr(String ip6Addr) {
+ this.ip6Addr = ip6Addr;
+ return this;
+ }
+
+ /**
+ * IP6Addr is the IPv6 address
+ * @return ip6Addr
+ **/
+ @ApiModelProperty(value = "IP6Addr is the IPv6 address")
+ public String getIp6Addr() {
+ return ip6Addr;
+ }
+
+ public void setIp6Addr(String ip6Addr) {
+ this.ip6Addr = ip6Addr;
+ }
+
+ public ModelsGateway lastSeenMillis(Long lastSeenMillis) {
+ this.lastSeenMillis = lastSeenMillis;
+ return this;
+ }
+
+ /**
+ * LastSeenMillis is a unix time in milliseconds representing the last time we received a heartbeat update from this gateway
+ * @return lastSeenMillis
+ **/
+ @ApiModelProperty(value = "LastSeenMillis is a unix time in milliseconds representing the last time we received a heartbeat update from this gateway")
+ public Long getLastSeenMillis() {
+ return lastSeenMillis;
+ }
+
+ public void setLastSeenMillis(Long lastSeenMillis) {
+ this.lastSeenMillis = lastSeenMillis;
+ }
+
+ public ModelsGateway load(BigDecimal load) {
+ this.load = load;
+ return this;
+ }
+
+ /**
+ * Load is the fractional load received from the menshen agent. For the time being it is a synthethic metric that takes into account number of clients and network information for the node.
+ * @return load
+ **/
+ @ApiModelProperty(value = "Load is the fractional load received from the menshen agent. For the time being it is a synthethic metric that takes into account number of clients and network information for the node.")
+ public BigDecimal getLoad() {
+ return load;
+ }
+
+ public void setLoad(BigDecimal load) {
+ this.load = load;
+ }
+
+ public ModelsGateway location(String location) {
+ this.location = location;
+ return this;
+ }
+
+ /**
+ * Location is the canonical label for the location of the gateway.
+ * @return location
+ **/
+ @ApiModelProperty(value = "Location is the canonical label for the location of the gateway.")
+ public String getLocation() {
+ return location;
+ }
+
+ public void setLocation(String location) {
+ this.location = location;
+ }
+
+ public ModelsGateway overloaded(Boolean overloaded) {
+ this.overloaded = overloaded;
+ return this;
+ }
+
+ /**
+ * Overloaded should be set to true if the fractional load is above threshold.
+ * @return overloaded
+ **/
+ @ApiModelProperty(value = "Overloaded should be set to true if the fractional load is above threshold.")
+ public Boolean isOverloaded() {
+ return overloaded;
+ }
+
+ public void setOverloaded(Boolean overloaded) {
+ this.overloaded = overloaded;
+ }
+
+ public ModelsGateway port(Integer port) {
+ this.port = port;
+ return this;
+ }
+
+ /**
+ * The (primary) port this gateway is listening on.
+ * @return port
+ **/
+ @ApiModelProperty(value = "The (primary) port this gateway is listening on.")
+ public Integer getPort() {
+ return port;
+ }
+
+ public void setPort(Integer port) {
+ this.port = port;
+ }
+
+ public ModelsGateway transport(String transport) {
+ this.transport = transport;
+ return this;
+ }
+
+ /**
+ * TCP, UDP, KCP or Quic. This was called \&quot;protocol\&quot; in previous versions of the API.
+ * @return transport
+ **/
+ @ApiModelProperty(value = "TCP, UDP, KCP or Quic. This was called \"protocol\" in previous versions of the API.")
+ public String getTransport() {
+ return transport;
+ }
+
+ public void setTransport(String transport) {
+ this.transport = transport;
+ }
+
+ public ModelsGateway type(String type) {
+ this.type = type;
+ return this;
+ }
+
+ /**
+ * Type is the type of gateway. The only valid type as of 2023 is openvpn.
+ * @return type
+ **/
+ @ApiModelProperty(value = "Type is the type of gateway. The only valid type as of 2023 is openvpn.")
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+
+ @Override
+ public boolean equals(java.lang.Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ModelsGateway modelsGateway = (ModelsGateway) o;
+ return Objects.equals(this.bucket, modelsGateway.bucket) &&
+ Objects.equals(this.experimental, modelsGateway.experimental) &&
+ Objects.equals(this.healthy, modelsGateway.healthy) &&
+ Objects.equals(this.host, modelsGateway.host) &&
+ Objects.equals(this.ipAddr, modelsGateway.ipAddr) &&
+ Objects.equals(this.ip6Addr, modelsGateway.ip6Addr) &&
+ Objects.equals(this.lastSeenMillis, modelsGateway.lastSeenMillis) &&
+ Objects.equals(this.load, modelsGateway.load) &&
+ Objects.equals(this.location, modelsGateway.location) &&
+ Objects.equals(this.overloaded, modelsGateway.overloaded) &&
+ Objects.equals(this.port, modelsGateway.port) &&
+ Objects.equals(this.transport, modelsGateway.transport) &&
+ Objects.equals(this.type, modelsGateway.type);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(bucket, experimental, healthy, host, ipAddr, ip6Addr, lastSeenMillis, load, location, overloaded, port, transport, type);
+ }
+
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class ModelsGateway {\n");
+
+ sb.append(" bucket: ").append(toIndentedString(bucket)).append("\n");
+ sb.append(" experimental: ").append(toIndentedString(experimental)).append("\n");
+ sb.append(" healthy: ").append(toIndentedString(healthy)).append("\n");
+ sb.append(" host: ").append(toIndentedString(host)).append("\n");
+ sb.append(" ipAddr: ").append(toIndentedString(ipAddr)).append("\n");
+ sb.append(" ip6Addr: ").append(toIndentedString(ip6Addr)).append("\n");
+ sb.append(" lastSeenMillis: ").append(toIndentedString(lastSeenMillis)).append("\n");
+ sb.append(" load: ").append(toIndentedString(load)).append("\n");
+ sb.append(" location: ").append(toIndentedString(location)).append("\n");
+ sb.append(" overloaded: ").append(toIndentedString(overloaded)).append("\n");
+ sb.append(" port: ").append(toIndentedString(port)).append("\n");
+ sb.append(" transport: ").append(toIndentedString(transport)).append("\n");
+ sb.append(" type: ").append(toIndentedString(type)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(java.lang.Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+
+}
+
diff --git a/app/src/main/java/io/swagger/client/model/ModelsLocation.java b/app/src/main/java/io/swagger/client/model/ModelsLocation.java
new file mode 100644
index 00000000..adda676b
--- /dev/null
+++ b/app/src/main/java/io/swagger/client/model/ModelsLocation.java
@@ -0,0 +1,301 @@
+/*
+ * Menshen API
+ * This is a LEAP VPN Service API
+ *
+ * OpenAPI spec version: 0.5.2
+ *
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+
+
+package io.swagger.client.model;
+
+import java.util.Objects;
+import java.util.Arrays;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import java.io.IOException;
+
+/**
+ * ModelsLocation
+ */
+
+public class ModelsLocation {
+ @SerializedName("country_code")
+ private String countryCode = null;
+
+ @SerializedName("display_name")
+ private String displayName = null;
+
+ @SerializedName("has_bridges")
+ private Boolean hasBridges = null;
+
+ @SerializedName("healthy")
+ private Boolean healthy = null;
+
+ @SerializedName("hemisphere")
+ private String hemisphere = null;
+
+ @SerializedName("label")
+ private String label = null;
+
+ @SerializedName("lat")
+ private String lat = null;
+
+ @SerializedName("lon")
+ private String lon = null;
+
+ @SerializedName("region")
+ private String region = null;
+
+ @SerializedName("timezone")
+ private String timezone = null;
+
+ public ModelsLocation countryCode(String countryCode) {
+ this.countryCode = countryCode;
+ return this;
+ }
+
+ /**
+ * CountryCode is the two-character country ISO identifier (uppercase).
+ * @return countryCode
+ **/
+ @ApiModelProperty(value = "CountryCode is the two-character country ISO identifier (uppercase).")
+ public String getCountryCode() {
+ return countryCode;
+ }
+
+ public void setCountryCode(String countryCode) {
+ this.countryCode = countryCode;
+ }
+
+ public ModelsLocation displayName(String displayName) {
+ this.displayName = displayName;
+ return this;
+ }
+
+ /**
+ * DisplayName is the user-facing string for a given location.
+ * @return displayName
+ **/
+ @ApiModelProperty(value = "DisplayName is the user-facing string for a given location.")
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
+ public ModelsLocation hasBridges(Boolean hasBridges) {
+ this.hasBridges = hasBridges;
+ return this;
+ }
+
+ /**
+ * Any location that has at least one bridge configured will set this to true.
+ * @return hasBridges
+ **/
+ @ApiModelProperty(value = "Any location that has at least one bridge configured will set this to true.")
+ public Boolean isHasBridges() {
+ return hasBridges;
+ }
+
+ public void setHasBridges(Boolean hasBridges) {
+ this.hasBridges = hasBridges;
+ }
+
+ public ModelsLocation healthy(Boolean healthy) {
+ this.healthy = healthy;
+ return this;
+ }
+
+ /**
+ * TODO Not used right now, but intended to signal when a location has all of their nodes overwhelmed.
+ * @return healthy
+ **/
+ @ApiModelProperty(value = "TODO Not used right now, but intended to signal when a location has all of their nodes overwhelmed.")
+ public Boolean isHealthy() {
+ return healthy;
+ }
+
+ public void setHealthy(Boolean healthy) {
+ this.healthy = healthy;
+ }
+
+ public ModelsLocation hemisphere(String hemisphere) {
+ this.hemisphere = hemisphere;
+ return this;
+ }
+
+ /**
+ * Hemisphere is a legacy label for a gateway. The rationale was once intended to be to allocate gateways for an hemisphere with certain regional \&quot;fairness\&quot;, even if they&#39;re geographically located in a different region. We might want to set this on the Gateway or Bridge, not in the Location itself...
+ * @return hemisphere
+ **/
+ @ApiModelProperty(value = "Hemisphere is a legacy label for a gateway. The rationale was once intended to be to allocate gateways for an hemisphere with certain regional \"fairness\", even if they're geographically located in a different region. We might want to set this on the Gateway or Bridge, not in the Location itself...")
+ public String getHemisphere() {
+ return hemisphere;
+ }
+
+ public void setHemisphere(String hemisphere) {
+ this.hemisphere = hemisphere;
+ }
+
+ public ModelsLocation label(String label) {
+ this.label = label;
+ return this;
+ }
+
+ /**
+ * Label is the short representation of a location, used internally.
+ * @return label
+ **/
+ @ApiModelProperty(value = "Label is the short representation of a location, used internally.")
+ public String getLabel() {
+ return label;
+ }
+
+ public void setLabel(String label) {
+ this.label = label;
+ }
+
+ public ModelsLocation lat(String lat) {
+ this.lat = lat;
+ return this;
+ }
+
+ /**
+ * Lat is the latitude for the location.
+ * @return lat
+ **/
+ @ApiModelProperty(value = "Lat is the latitude for the location.")
+ public String getLat() {
+ return lat;
+ }
+
+ public void setLat(String lat) {
+ this.lat = lat;
+ }
+
+ public ModelsLocation lon(String lon) {
+ this.lon = lon;
+ return this;
+ }
+
+ /**
+ * Lon is the longitude for the location.
+ * @return lon
+ **/
+ @ApiModelProperty(value = "Lon is the longitude for the location.")
+ public String getLon() {
+ return lon;
+ }
+
+ public void setLon(String lon) {
+ this.lon = lon;
+ }
+
+ public ModelsLocation region(String region) {
+ this.region = region;
+ return this;
+ }
+
+ /**
+ * Region is the continental region this gateway is assigned to. Not used at the moment, intended to use a label from the 7-continent model.
+ * @return region
+ **/
+ @ApiModelProperty(value = "Region is the continental region this gateway is assigned to. Not used at the moment, intended to use a label from the 7-continent model.")
+ public String getRegion() {
+ return region;
+ }
+
+ public void setRegion(String region) {
+ this.region = region;
+ }
+
+ public ModelsLocation timezone(String timezone) {
+ this.timezone = timezone;
+ return this;
+ }
+
+ /**
+ * Timezone is the TZ for the location (-1, 0, +1, ...)
+ * @return timezone
+ **/
+ @ApiModelProperty(value = "Timezone is the TZ for the location (-1, 0, +1, ...)")
+ public String getTimezone() {
+ return timezone;
+ }
+
+ public void setTimezone(String timezone) {
+ this.timezone = timezone;
+ }
+
+
+ @Override
+ public boolean equals(java.lang.Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ModelsLocation modelsLocation = (ModelsLocation) o;
+ return Objects.equals(this.countryCode, modelsLocation.countryCode) &&
+ Objects.equals(this.displayName, modelsLocation.displayName) &&
+ Objects.equals(this.hasBridges, modelsLocation.hasBridges) &&
+ Objects.equals(this.healthy, modelsLocation.healthy) &&
+ Objects.equals(this.hemisphere, modelsLocation.hemisphere) &&
+ Objects.equals(this.label, modelsLocation.label) &&
+ Objects.equals(this.lat, modelsLocation.lat) &&
+ Objects.equals(this.lon, modelsLocation.lon) &&
+ Objects.equals(this.region, modelsLocation.region) &&
+ Objects.equals(this.timezone, modelsLocation.timezone);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(countryCode, displayName, hasBridges, healthy, hemisphere, label, lat, lon, region, timezone);
+ }
+
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class ModelsLocation {\n");
+
+ sb.append(" countryCode: ").append(toIndentedString(countryCode)).append("\n");
+ sb.append(" displayName: ").append(toIndentedString(displayName)).append("\n");
+ sb.append(" hasBridges: ").append(toIndentedString(hasBridges)).append("\n");
+ sb.append(" healthy: ").append(toIndentedString(healthy)).append("\n");
+ sb.append(" hemisphere: ").append(toIndentedString(hemisphere)).append("\n");
+ sb.append(" label: ").append(toIndentedString(label)).append("\n");
+ sb.append(" lat: ").append(toIndentedString(lat)).append("\n");
+ sb.append(" lon: ").append(toIndentedString(lon)).append("\n");
+ sb.append(" region: ").append(toIndentedString(region)).append("\n");
+ sb.append(" timezone: ").append(toIndentedString(timezone)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(java.lang.Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+
+}
+
diff --git a/app/src/main/java/io/swagger/client/model/ModelsProvider.java b/app/src/main/java/io/swagger/client/model/ModelsProvider.java
new file mode 100644
index 00000000..34b54510
--- /dev/null
+++ b/app/src/main/java/io/swagger/client/model/ModelsProvider.java
@@ -0,0 +1,584 @@
+/*
+ * Menshen API
+ * This is a LEAP VPN Service API
+ *
+ * OpenAPI spec version: 0.5.2
+ *
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+
+
+package io.swagger.client.model;
+
+import java.util.Objects;
+import java.util.Arrays;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import io.swagger.client.model.ModelsProviderService;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * ModelsProvider
+ */
+
+public class ModelsProvider {
+ @SerializedName("api_uri")
+ private String apiUri = null;
+
+ @SerializedName("api_version")
+ private String apiVersion = null;
+
+ @SerializedName("api_versions")
+ private List<String> apiVersions = null;
+
+ @SerializedName("ask_for_donations")
+ private Boolean askForDonations = null;
+
+ @SerializedName("ca_cert_fingerprint")
+ private String caCertFingerprint = null;
+
+ @SerializedName("ca_cert_uri")
+ private String caCertUri = null;
+
+ @SerializedName("country_code_lookup_url")
+ private String countryCodeLookupUrl = null;
+
+ @SerializedName("default_language")
+ private String defaultLanguage = null;
+
+ @SerializedName("description")
+ private Map<String, String> description = null;
+
+ @SerializedName("domain")
+ private String domain = null;
+
+ @SerializedName("donate_period")
+ private String donatePeriod = null;
+
+ @SerializedName("donate_url")
+ private String donateUrl = null;
+
+ @SerializedName("info_url")
+ private String infoUrl = null;
+
+ @SerializedName("languages")
+ private List<String> languages = null;
+
+ @SerializedName("motd_url")
+ private String motdUrl = null;
+
+ @SerializedName("name")
+ private Map<String, String> name = null;
+
+ @SerializedName("service")
+ private ModelsProviderService service = null;
+
+ @SerializedName("services")
+ private List<String> services = null;
+
+ @SerializedName("stun_servers")
+ private List<String> stunServers = null;
+
+ @SerializedName("tos_url")
+ private String tosUrl = null;
+
+ public ModelsProvider apiUri(String apiUri) {
+ this.apiUri = apiUri;
+ return this;
+ }
+
+ /**
+ * URL of the API endpoints
+ * @return apiUri
+ **/
+ @ApiModelProperty(value = "URL of the API endpoints")
+ public String getApiUri() {
+ return apiUri;
+ }
+
+ public void setApiUri(String apiUri) {
+ this.apiUri = apiUri;
+ }
+
+ public ModelsProvider apiVersion(String apiVersion) {
+ this.apiVersion = apiVersion;
+ return this;
+ }
+
+ /**
+ * oldest supported api version deprecated: kept for backwards compatibility. Replaced by api_versions.
+ * @return apiVersion
+ **/
+ @ApiModelProperty(value = "oldest supported api version deprecated: kept for backwards compatibility. Replaced by api_versions.")
+ public String getApiVersion() {
+ return apiVersion;
+ }
+
+ public void setApiVersion(String apiVersion) {
+ this.apiVersion = apiVersion;
+ }
+
+ public ModelsProvider apiVersions(List<String> apiVersions) {
+ this.apiVersions = apiVersions;
+ return this;
+ }
+
+ public ModelsProvider addApiVersionsItem(String apiVersionsItem) {
+ if (this.apiVersions == null) {
+ this.apiVersions = new ArrayList<String>();
+ }
+ this.apiVersions.add(apiVersionsItem);
+ return this;
+ }
+
+ /**
+ * all API versions the provider supports
+ * @return apiVersions
+ **/
+ @ApiModelProperty(value = "all API versions the provider supports")
+ public List<String> getApiVersions() {
+ return apiVersions;
+ }
+
+ public void setApiVersions(List<String> apiVersions) {
+ this.apiVersions = apiVersions;
+ }
+
+ public ModelsProvider askForDonations(Boolean askForDonations) {
+ this.askForDonations = askForDonations;
+ return this;
+ }
+
+ /**
+ * Flag indicating whether to show regularly a donation reminder
+ * @return askForDonations
+ **/
+ @ApiModelProperty(value = "Flag indicating whether to show regularly a donation reminder")
+ public Boolean isAskForDonations() {
+ return askForDonations;
+ }
+
+ public void setAskForDonations(Boolean askForDonations) {
+ this.askForDonations = askForDonations;
+ }
+
+ public ModelsProvider caCertFingerprint(String caCertFingerprint) {
+ this.caCertFingerprint = caCertFingerprint;
+ return this;
+ }
+
+ /**
+ * fingerprint of CA cert used to setup TLS sessions during VPN setup (and up to API version 3 for API communication) deprecated: kept for backwards compatibility
+ * @return caCertFingerprint
+ **/
+ @ApiModelProperty(value = "fingerprint of CA cert used to setup TLS sessions during VPN setup (and up to API version 3 for API communication) deprecated: kept for backwards compatibility")
+ public String getCaCertFingerprint() {
+ return caCertFingerprint;
+ }
+
+ public void setCaCertFingerprint(String caCertFingerprint) {
+ this.caCertFingerprint = caCertFingerprint;
+ }
+
+ public ModelsProvider caCertUri(String caCertUri) {
+ this.caCertUri = caCertUri;
+ return this;
+ }
+
+ /**
+ * URL to fetch the CA cert used to setup TLS sessions during VPN setup (and up to API version 3 for API communication) deprecated: kept for backwards compatibility
+ * @return caCertUri
+ **/
+ @ApiModelProperty(value = "URL to fetch the CA cert used to setup TLS sessions during VPN setup (and up to API version 3 for API communication) deprecated: kept for backwards compatibility")
+ public String getCaCertUri() {
+ return caCertUri;
+ }
+
+ public void setCaCertUri(String caCertUri) {
+ this.caCertUri = caCertUri;
+ }
+
+ public ModelsProvider countryCodeLookupUrl(String countryCodeLookupUrl) {
+ this.countryCodeLookupUrl = countryCodeLookupUrl;
+ return this;
+ }
+
+ /**
+ * URL of a service that returns a country code for an ip address. If empty, OONI backend is used
+ * @return countryCodeLookupUrl
+ **/
+ @ApiModelProperty(value = "URL of a service that returns a country code for an ip address. If empty, OONI backend is used")
+ public String getCountryCodeLookupUrl() {
+ return countryCodeLookupUrl;
+ }
+
+ public void setCountryCodeLookupUrl(String countryCodeLookupUrl) {
+ this.countryCodeLookupUrl = countryCodeLookupUrl;
+ }
+
+ public ModelsProvider defaultLanguage(String defaultLanguage) {
+ this.defaultLanguage = defaultLanguage;
+ return this;
+ }
+
+ /**
+ * Default language this provider uses to show infos and provider messages
+ * @return defaultLanguage
+ **/
+ @ApiModelProperty(value = "Default language this provider uses to show infos and provider messages")
+ public String getDefaultLanguage() {
+ return defaultLanguage;
+ }
+
+ public void setDefaultLanguage(String defaultLanguage) {
+ this.defaultLanguage = defaultLanguage;
+ }
+
+ public ModelsProvider description(Map<String, String> description) {
+ this.description = description;
+ return this;
+ }
+
+ public ModelsProvider putDescriptionItem(String key, String descriptionItem) {
+ if (this.description == null) {
+ this.description = new HashMap<String, String>();
+ }
+ this.description.put(key, descriptionItem);
+ return this;
+ }
+
+ /**
+ * Short description about the provider
+ * @return description
+ **/
+ @ApiModelProperty(value = "Short description about the provider")
+ public Map<String, String> getDescription() {
+ return description;
+ }
+
+ public void setDescription(Map<String, String> description) {
+ this.description = description;
+ }
+
+ public ModelsProvider domain(String domain) {
+ this.domain = domain;
+ return this;
+ }
+
+ /**
+ * Domain of the provider
+ * @return domain
+ **/
+ @ApiModelProperty(value = "Domain of the provider")
+ public String getDomain() {
+ return domain;
+ }
+
+ public void setDomain(String domain) {
+ this.domain = domain;
+ }
+
+ public ModelsProvider donatePeriod(String donatePeriod) {
+ this.donatePeriod = donatePeriod;
+ return this;
+ }
+
+ /**
+ * Number of days until a donation reminder reappears
+ * @return donatePeriod
+ **/
+ @ApiModelProperty(value = "Number of days until a donation reminder reappears")
+ public String getDonatePeriod() {
+ return donatePeriod;
+ }
+
+ public void setDonatePeriod(String donatePeriod) {
+ this.donatePeriod = donatePeriod;
+ }
+
+ public ModelsProvider donateUrl(String donateUrl) {
+ this.donateUrl = donateUrl;
+ return this;
+ }
+
+ /**
+ * URL to the donation website
+ * @return donateUrl
+ **/
+ @ApiModelProperty(value = "URL to the donation website")
+ public String getDonateUrl() {
+ return donateUrl;
+ }
+
+ public void setDonateUrl(String donateUrl) {
+ this.donateUrl = donateUrl;
+ }
+
+ public ModelsProvider infoUrl(String infoUrl) {
+ this.infoUrl = infoUrl;
+ return this;
+ }
+
+ /**
+ * URL to general provider website
+ * @return infoUrl
+ **/
+ @ApiModelProperty(value = "URL to general provider website")
+ public String getInfoUrl() {
+ return infoUrl;
+ }
+
+ public void setInfoUrl(String infoUrl) {
+ this.infoUrl = infoUrl;
+ }
+
+ public ModelsProvider languages(List<String> languages) {
+ this.languages = languages;
+ return this;
+ }
+
+ public ModelsProvider addLanguagesItem(String languagesItem) {
+ if (this.languages == null) {
+ this.languages = new ArrayList<String>();
+ }
+ this.languages.add(languagesItem);
+ return this;
+ }
+
+ /**
+ * Languages the provider supports to show infos and provider messages
+ * @return languages
+ **/
+ @ApiModelProperty(value = "Languages the provider supports to show infos and provider messages")
+ public List<String> getLanguages() {
+ return languages;
+ }
+
+ public void setLanguages(List<String> languages) {
+ this.languages = languages;
+ }
+
+ public ModelsProvider motdUrl(String motdUrl) {
+ this.motdUrl = motdUrl;
+ return this;
+ }
+
+ /**
+ * URL to the message of the day service
+ * @return motdUrl
+ **/
+ @ApiModelProperty(value = "URL to the message of the day service")
+ public String getMotdUrl() {
+ return motdUrl;
+ }
+
+ public void setMotdUrl(String motdUrl) {
+ this.motdUrl = motdUrl;
+ }
+
+ public ModelsProvider name(Map<String, String> name) {
+ this.name = name;
+ return this;
+ }
+
+ public ModelsProvider putNameItem(String key, String nameItem) {
+ if (this.name == null) {
+ this.name = new HashMap<String, String>();
+ }
+ this.name.put(key, nameItem);
+ return this;
+ }
+
+ /**
+ * Provider name
+ * @return name
+ **/
+ @ApiModelProperty(value = "Provider name")
+ public Map<String, String> getName() {
+ return name;
+ }
+
+ public void setName(Map<String, String> name) {
+ this.name = name;
+ }
+
+ public ModelsProvider service(ModelsProviderService service) {
+ this.service = service;
+ return this;
+ }
+
+ /**
+ * Get service
+ * @return service
+ **/
+ @ApiModelProperty(value = "")
+ public ModelsProviderService getService() {
+ return service;
+ }
+
+ public void setService(ModelsProviderService service) {
+ this.service = service;
+ }
+
+ public ModelsProvider services(List<String> services) {
+ this.services = services;
+ return this;
+ }
+
+ public ModelsProvider addServicesItem(String servicesItem) {
+ if (this.services == null) {
+ this.services = new ArrayList<String>();
+ }
+ this.services.add(servicesItem);
+ return this;
+ }
+
+ /**
+ * List of services the provider offers, currently only openvpn
+ * @return services
+ **/
+ @ApiModelProperty(value = "List of services the provider offers, currently only openvpn")
+ public List<String> getServices() {
+ return services;
+ }
+
+ public void setServices(List<String> services) {
+ this.services = services;
+ }
+
+ public ModelsProvider stunServers(List<String> stunServers) {
+ this.stunServers = stunServers;
+ return this;
+ }
+
+ public ModelsProvider addStunServersItem(String stunServersItem) {
+ if (this.stunServers == null) {
+ this.stunServers = new ArrayList<String>();
+ }
+ this.stunServers.add(stunServersItem);
+ return this;
+ }
+
+ /**
+ * list of STUN servers (format: ip/hostname:port) servers to get current ip address can consist of self hosted STUN servers, public ones or a combination of both. GeolocationLookup is only done when the list of STUNServers is not empty
+ * @return stunServers
+ **/
+ @ApiModelProperty(value = "list of STUN servers (format: ip/hostname:port) servers to get current ip address can consist of self hosted STUN servers, public ones or a combination of both. GeolocationLookup is only done when the list of STUNServers is not empty")
+ public List<String> getStunServers() {
+ return stunServers;
+ }
+
+ public void setStunServers(List<String> stunServers) {
+ this.stunServers = stunServers;
+ }
+
+ public ModelsProvider tosUrl(String tosUrl) {
+ this.tosUrl = tosUrl;
+ return this;
+ }
+
+ /**
+ * URL to Terms of Service website
+ * @return tosUrl
+ **/
+ @ApiModelProperty(value = "URL to Terms of Service website")
+ public String getTosUrl() {
+ return tosUrl;
+ }
+
+ public void setTosUrl(String tosUrl) {
+ this.tosUrl = tosUrl;
+ }
+
+
+ @Override
+ public boolean equals(java.lang.Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ModelsProvider modelsProvider = (ModelsProvider) o;
+ return Objects.equals(this.apiUri, modelsProvider.apiUri) &&
+ Objects.equals(this.apiVersion, modelsProvider.apiVersion) &&
+ Objects.equals(this.apiVersions, modelsProvider.apiVersions) &&
+ Objects.equals(this.askForDonations, modelsProvider.askForDonations) &&
+ Objects.equals(this.caCertFingerprint, modelsProvider.caCertFingerprint) &&
+ Objects.equals(this.caCertUri, modelsProvider.caCertUri) &&
+ Objects.equals(this.countryCodeLookupUrl, modelsProvider.countryCodeLookupUrl) &&
+ Objects.equals(this.defaultLanguage, modelsProvider.defaultLanguage) &&
+ Objects.equals(this.description, modelsProvider.description) &&
+ Objects.equals(this.domain, modelsProvider.domain) &&
+ Objects.equals(this.donatePeriod, modelsProvider.donatePeriod) &&
+ Objects.equals(this.donateUrl, modelsProvider.donateUrl) &&
+ Objects.equals(this.infoUrl, modelsProvider.infoUrl) &&
+ Objects.equals(this.languages, modelsProvider.languages) &&
+ Objects.equals(this.motdUrl, modelsProvider.motdUrl) &&
+ Objects.equals(this.name, modelsProvider.name) &&
+ Objects.equals(this.service, modelsProvider.service) &&
+ Objects.equals(this.services, modelsProvider.services) &&
+ Objects.equals(this.stunServers, modelsProvider.stunServers) &&
+ Objects.equals(this.tosUrl, modelsProvider.tosUrl);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(apiUri, apiVersion, apiVersions, askForDonations, caCertFingerprint, caCertUri, countryCodeLookupUrl, defaultLanguage, description, domain, donatePeriod, donateUrl, infoUrl, languages, motdUrl, name, service, services, stunServers, tosUrl);
+ }
+
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class ModelsProvider {\n");
+
+ sb.append(" apiUri: ").append(toIndentedString(apiUri)).append("\n");
+ sb.append(" apiVersion: ").append(toIndentedString(apiVersion)).append("\n");
+ sb.append(" apiVersions: ").append(toIndentedString(apiVersions)).append("\n");
+ sb.append(" askForDonations: ").append(toIndentedString(askForDonations)).append("\n");
+ sb.append(" caCertFingerprint: ").append(toIndentedString(caCertFingerprint)).append("\n");
+ sb.append(" caCertUri: ").append(toIndentedString(caCertUri)).append("\n");
+ sb.append(" countryCodeLookupUrl: ").append(toIndentedString(countryCodeLookupUrl)).append("\n");
+ sb.append(" defaultLanguage: ").append(toIndentedString(defaultLanguage)).append("\n");
+ sb.append(" description: ").append(toIndentedString(description)).append("\n");
+ sb.append(" domain: ").append(toIndentedString(domain)).append("\n");
+ sb.append(" donatePeriod: ").append(toIndentedString(donatePeriod)).append("\n");
+ sb.append(" donateUrl: ").append(toIndentedString(donateUrl)).append("\n");
+ sb.append(" infoUrl: ").append(toIndentedString(infoUrl)).append("\n");
+ sb.append(" languages: ").append(toIndentedString(languages)).append("\n");
+ sb.append(" motdUrl: ").append(toIndentedString(motdUrl)).append("\n");
+ sb.append(" name: ").append(toIndentedString(name)).append("\n");
+ sb.append(" service: ").append(toIndentedString(service)).append("\n");
+ sb.append(" services: ").append(toIndentedString(services)).append("\n");
+ sb.append(" stunServers: ").append(toIndentedString(stunServers)).append("\n");
+ sb.append(" tosUrl: ").append(toIndentedString(tosUrl)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(java.lang.Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+
+}
+
diff --git a/app/src/main/java/io/swagger/client/model/ModelsProviderService.java b/app/src/main/java/io/swagger/client/model/ModelsProviderService.java
new file mode 100644
index 00000000..ef13b60d
--- /dev/null
+++ b/app/src/main/java/io/swagger/client/model/ModelsProviderService.java
@@ -0,0 +1,118 @@
+/*
+ * Menshen API
+ * This is a LEAP VPN Service API
+ *
+ * OpenAPI spec version: 0.5.2
+ *
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+
+
+package io.swagger.client.model;
+
+import java.util.Objects;
+import java.util.Arrays;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import java.io.IOException;
+
+/**
+ * Operational properties which describe how the provider offers the service
+ */
+@ApiModel(description = "Operational properties which describe how the provider offers the service")
+
+public class ModelsProviderService {
+ @SerializedName("allow_anonymous")
+ private Boolean allowAnonymous = null;
+
+ @SerializedName("allow_registration")
+ private Boolean allowRegistration = null;
+
+ public ModelsProviderService allowAnonymous(Boolean allowAnonymous) {
+ this.allowAnonymous = allowAnonymous;
+ return this;
+ }
+
+ /**
+ * Flag indicating if anonymous usage without registration is allowed deprecated: kept for backwards compatibility
+ * @return allowAnonymous
+ **/
+ @ApiModelProperty(value = "Flag indicating if anonymous usage without registration is allowed deprecated: kept for backwards compatibility")
+ public Boolean isAllowAnonymous() {
+ return allowAnonymous;
+ }
+
+ public void setAllowAnonymous(Boolean allowAnonymous) {
+ this.allowAnonymous = allowAnonymous;
+ }
+
+ public ModelsProviderService allowRegistration(Boolean allowRegistration) {
+ this.allowRegistration = allowRegistration;
+ return this;
+ }
+
+ /**
+ * Flag indicating if the provider supports user registration deprecated: kept for backwards compatibility
+ * @return allowRegistration
+ **/
+ @ApiModelProperty(value = "Flag indicating if the provider supports user registration deprecated: kept for backwards compatibility")
+ public Boolean isAllowRegistration() {
+ return allowRegistration;
+ }
+
+ public void setAllowRegistration(Boolean allowRegistration) {
+ this.allowRegistration = allowRegistration;
+ }
+
+
+ @Override
+ public boolean equals(java.lang.Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ModelsProviderService modelsProviderService = (ModelsProviderService) o;
+ return Objects.equals(this.allowAnonymous, modelsProviderService.allowAnonymous) &&
+ Objects.equals(this.allowRegistration, modelsProviderService.allowRegistration);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(allowAnonymous, allowRegistration);
+ }
+
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class ModelsProviderService {\n");
+
+ sb.append(" allowAnonymous: ").append(toIndentedString(allowAnonymous)).append("\n");
+ sb.append(" allowRegistration: ").append(toIndentedString(allowRegistration)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(java.lang.Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+
+}
+
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java b/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java
index c7e12491..6b3ba348 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java
@@ -35,8 +35,10 @@ import androidx.lifecycle.ProcessLifecycleOwner;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.multidex.MultiDexApplication;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.conscrypt.Conscrypt;
+import java.security.Provider;
import java.security.Security;
import se.leap.bitmaskclient.BuildConfig;
@@ -70,7 +72,14 @@ public class BitmaskApp extends MultiDexApplication implements DefaultLifecycleO
super.onCreate();
// Normal app init code...*/
PRNGFixes.apply();
- Security.insertProviderAt(Conscrypt.newProvider(), 1);
+ final Provider provider = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME);
+ // Replace Android's own BC provider
+ if (!provider.getClass().equals(BouncyCastleProvider.class)) {
+ Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
+ Security.insertProviderAt(new BouncyCastleProvider(), 1);
+ }
+ Security.insertProviderAt(Conscrypt.newProvider(), 2);
+
preferenceHelper = new PreferenceHelper(this);
providerObservable = ProviderObservable.getInstance();
providerObservable.updateProvider(getSavedProviderFromSharedPreferences());
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java b/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java
index 21fc81e4..5363f16e 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java
@@ -51,6 +51,7 @@ import se.leap.bitmaskclient.base.models.ProviderObservable;
import se.leap.bitmaskclient.base.utils.DateHelper;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
import se.leap.bitmaskclient.eip.EipCommand;
+import se.leap.bitmaskclient.providersetup.ProviderSetupObservable;
import se.leap.bitmaskclient.providersetup.activities.SetupActivity;
/**
@@ -178,9 +179,10 @@ public class StartActivity extends Activity{
if ((hasNewFeature(FeatureVersionCode.CALYX_PROVIDER_LILYPAD_UPDATE) && (
getPackageName().equals("org.calyxinstitute.vpn") ||
ProviderObservable.getInstance().getCurrentProvider().getDomain().equals("calyx.net"))) ||
- hasNewFeature(FeatureVersionCode.RISEUP_PROVIDER_LILYPAD_UPDATE) && (
+ (hasNewFeature(FeatureVersionCode.RISEUP_PROVIDER_LILYPAD_UPDATE) || hasNewFeature(FeatureVersionCode.RISEUP_PROVIDER_LILYPAD_UPDATE_v2)
+ && (
getPackageName().equals("se.leap.riseupvpn") ||
- ProviderObservable.getInstance().getCurrentProvider().getDomain().equals("riseup.net"))) {
+ ProviderObservable.getInstance().getCurrentProvider().getDomain().equals("riseup.net")))) {
// deletion of current configured provider so that a new provider setup is triggered
Provider provider = ProviderObservable.getInstance().getCurrentProvider();
if (provider != null && !provider.isDefault()) {
@@ -267,6 +269,7 @@ public class StartActivity extends Activity{
}
private void showNextActivity(Provider provider) {
+ ProviderSetupObservable.cancel();
if (provider.shouldShowMotdSeen()) {
try {
IMessages messages = Motd.newMessages(provider.getMotdJsonString());
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/CensorshipCircumventionFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/CensorshipCircumventionFragment.java
new file mode 100644
index 00000000..fc561d48
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/CensorshipCircumventionFragment.java
@@ -0,0 +1,166 @@
+package se.leap.bitmaskclient.base.fragments;
+
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseObfs4;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseObfs4Kcp;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUsePortHopping;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseObfs4Quic;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseSnowflake;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.hasSnowflakePrefs;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.resetSnowflakeSettings;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.setUsePortHopping;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.setUseTunnel;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useBridges;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useSnowflake;
+import static se.leap.bitmaskclient.base.utils.ViewHelper.setActionBarSubtitle;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RadioButton;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import de.blinkt.openvpn.core.VpnStatus;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.models.ProviderObservable;
+import se.leap.bitmaskclient.databinding.FCensorshipCircumventionBinding;
+import se.leap.bitmaskclient.eip.EipCommand;
+
+public class CensorshipCircumventionFragment extends Fragment {
+ public static int DISCOVERY_AUTOMATICALLY = 100200000;
+ public static int DISCOVERY_SNOWFLAKE = 100200001;
+ public static int DISCOVERY_INVITE_PROXY = 100200002;
+
+ public static int TUNNELING_AUTOMATICALLY = 100300000;
+ public static int TUNNELING_OBFS4 = 100300001;
+ public static int TUNNELING_OBFS4_KCP = 100300002;
+ public static int TUNNELING_QUIC = 100300003;
+
+ private @NonNull FCensorshipCircumventionBinding binding;
+
+ public static CensorshipCircumventionFragment newInstance() {
+ CensorshipCircumventionFragment fragment = new CensorshipCircumventionFragment();
+ Bundle args = new Bundle();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ binding = FCensorshipCircumventionBinding.inflate(getLayoutInflater(), container, false);
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ setActionBarSubtitle(this, R.string.censorship_circumvention);
+ initDiscovery();
+ initTunneling();
+ initPortHopping();
+ }
+
+
+ private void initDiscovery() {
+ boolean hasIntroducer = ProviderObservable.getInstance().getCurrentProvider().hasIntroducer();
+ RadioButton automaticallyRadioButton = new RadioButton(binding.getRoot().getContext());
+ automaticallyRadioButton.setText(getText(R.string.automatically_select));
+ automaticallyRadioButton.setId(DISCOVERY_AUTOMATICALLY);
+ automaticallyRadioButton.setChecked(!hasSnowflakePrefs() && !hasIntroducer);
+ binding.discoveryRadioGroup.addView(automaticallyRadioButton);
+
+ RadioButton snowflakeRadioButton = new RadioButton(binding.getRoot().getContext());
+ snowflakeRadioButton.setText(getText(R.string.snowflake));
+ snowflakeRadioButton.setId(DISCOVERY_SNOWFLAKE);
+ snowflakeRadioButton.setChecked(!hasIntroducer && hasSnowflakePrefs() && getUseSnowflake());
+ binding.discoveryRadioGroup.addView(snowflakeRadioButton);
+
+ if (hasIntroducer) {
+ RadioButton inviteProxyRadioButton = new RadioButton(binding.getRoot().getContext());
+ inviteProxyRadioButton.setText(getText(R.string.invite_proxy));
+ inviteProxyRadioButton.setId(DISCOVERY_INVITE_PROXY);
+ inviteProxyRadioButton.setChecked(true);
+ binding.discoveryRadioGroup.addView(inviteProxyRadioButton);
+ }
+
+ binding.discoveryRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
+ useBridges(true);
+ if (checkedId == DISCOVERY_AUTOMATICALLY) {
+ resetSnowflakeSettings();
+ } else if (checkedId == DISCOVERY_SNOWFLAKE) {
+ useSnowflake(true);
+ } else if (checkedId == DISCOVERY_INVITE_PROXY) {
+ useSnowflake(false);
+ }
+ });
+ }
+
+ private void tryReconnectVpn() {
+ if (VpnStatus.isVPNActive()) {
+ EipCommand.startVPN(getContext(), false);
+ Toast.makeText(getContext(), R.string.reconnecting, Toast.LENGTH_LONG).show();
+ }
+ }
+
+
+ private void initTunneling() {
+ RadioButton noneRadioButton = new RadioButton(binding.getRoot().getContext());
+ noneRadioButton.setText(getText(R.string.automatically_select));
+ noneRadioButton.setChecked(!getUseObfs4() && !getUseObfs4Kcp() && !getUseObfs4Quic());
+ noneRadioButton.setId(TUNNELING_AUTOMATICALLY);
+ binding.tunnelingRadioGroup.addView(noneRadioButton);
+
+ if (ProviderObservable.getInstance().getCurrentProvider().supportsObfs4()) {
+ RadioButton obfs4RadioButton = new RadioButton(binding.getRoot().getContext());
+ obfs4RadioButton.setText(getText(R.string.tunnelling_obfs4));
+ obfs4RadioButton.setId(TUNNELING_OBFS4);
+ obfs4RadioButton.setChecked(getUseObfs4());
+ binding.tunnelingRadioGroup.addView(obfs4RadioButton);
+ }
+
+ if (ProviderObservable.getInstance().getCurrentProvider().supportsObfs4Kcp()) {
+ RadioButton obfs4KcpRadioButton = new RadioButton(binding.getRoot().getContext());
+ obfs4KcpRadioButton.setText(getText(R.string.tunnelling_obfs4_kcp));
+ obfs4KcpRadioButton.setId(TUNNELING_OBFS4_KCP);
+ obfs4KcpRadioButton.setChecked(getUseObfs4Kcp());
+ binding.tunnelingRadioGroup.addView(obfs4KcpRadioButton);
+ }
+
+ if (ProviderObservable.getInstance().getCurrentProvider().supportsObfs4Quic()) {
+ RadioButton obfs4QuicRadioButton = new RadioButton(binding.getRoot().getContext());
+ obfs4QuicRadioButton.setText(getText(R.string.tunnelling_quic));
+ obfs4QuicRadioButton.setId(TUNNELING_QUIC);
+ obfs4QuicRadioButton.setChecked(getUseObfs4Quic());
+ binding.tunnelingRadioGroup.addView(obfs4QuicRadioButton);
+ }
+
+ binding.tunnelingRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
+ useBridges(true);
+ setUseTunnel(checkedId);
+ tryReconnectVpn();
+ });
+ }
+
+ private void initPortHopping() {
+ binding.portHoppingSwitch.setVisibility(ProviderObservable.getInstance().getCurrentProvider().supportsObfs4Hop() ? View.VISIBLE : View.GONE);
+ binding.portHoppingSwitch.findViewById(R.id.material_icon).setVisibility(View.GONE);
+ binding.portHoppingSwitch.setChecked(getUsePortHopping());
+ binding.portHoppingSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (!buttonView.isPressed()) {
+ return;
+ }
+ useBridges(true);
+ setUsePortHopping(isChecked);
+ tryReconnectVpn();
+ });
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java
index fb93796e..76834332 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java
@@ -50,6 +50,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.core.content.ContextCompat;
+import androidx.core.os.BundleCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
@@ -116,7 +117,7 @@ public class EipFragment extends Fragment implements PropertyChangeListener {
Activity activity = getActivity();
if (activity != null) {
if (arguments != null) {
- provider = arguments.getParcelable(PROVIDER_KEY);
+ provider = BundleCompat.getParcelable(arguments, PROVIDER_KEY, Provider.class);
if (provider == null) {
handleNoProvider(activity);
} else {
@@ -307,7 +308,7 @@ public class EipFragment extends Fragment implements PropertyChangeListener {
Log.e(TAG, "context is null when trying to start VPN");
return;
}
- if (!provider.getGeoipUrl().isDefault() && provider.shouldUpdateGeoIpJson()) {
+ if (!provider.getGeoipUrl().isEmpty() && provider.shouldUpdateGeoIpJson()) {
Bundle bundle = new Bundle();
bundle.putBoolean(EIP_ACTION_START, true);
bundle.putBoolean(EIP_EARLY_ROUTES, false);
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java
index bb5a06c4..5cd6c2a0 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java
@@ -48,8 +48,6 @@ import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.WeakReference;
import java.util.List;
-import java.util.Observable;
-import java.util.Observer;
import de.blinkt.openvpn.core.VpnStatus;
import de.blinkt.openvpn.core.connection.Connection;
@@ -144,10 +142,11 @@ public class GatewaySelectionFragment extends Fragment implements PropertyChange
}
private void initBridgesHint(@NonNull View view) {
+ boolean allowResettingBridges = getUseBridges() && gatewaysManager.hasLocationsForOpenVPN();
bridgesHint = view.findViewById(R.id.manual_subtitle);
- bridgesHint.setVisibility(getUseBridges() ? VISIBLE : GONE);
+ bridgesHint.setVisibility(allowResettingBridges ? VISIBLE : GONE);
disableBridges = view.findViewById(R.id.disable_bridges);
- disableBridges.setVisibility(getUseBridges() ? VISIBLE : GONE);
+ disableBridges.setVisibility(allowResettingBridges ? VISIBLE : GONE);
disableBridges.setOnClickListener(v -> {
useBridges(false);
});
@@ -218,6 +217,7 @@ public class GatewaySelectionFragment extends Fragment implements PropertyChange
locationListAdapter.updateTransport(selectedTransport, gatewaysManager);
bridgesHint.setVisibility(showBridges ? VISIBLE : GONE);
disableBridges.setVisibility(showBridges ? VISIBLE : GONE);
+ updateRecommendedLocation();
}
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/LanguageSelectionFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/LanguageSelectionFragment.java
new file mode 100644
index 00000000..263a8d46
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/LanguageSelectionFragment.java
@@ -0,0 +1,166 @@
+package se.leap.bitmaskclient.base.fragments;
+
+import static se.leap.bitmaskclient.base.utils.ViewHelper.setActionBarSubtitle;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatDelegate;
+import androidx.core.os.LocaleListCompat;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.views.SimpleCheckBox;
+import se.leap.bitmaskclient.databinding.FLanguageSelectionBinding;
+import se.leap.bitmaskclient.databinding.VSelectTextListItemBinding;
+
+public class LanguageSelectionFragment extends BottomSheetDialogFragment {
+ static final String TAG = LanguageSelectionFragment.class.getSimpleName();
+ static final String SYSTEM_LOCALE = "systemLocale";
+ private FLanguageSelectionBinding binding;
+
+ public static LanguageSelectionFragment newInstance(Locale defaultLocale) {
+ LanguageSelectionFragment fragment = new LanguageSelectionFragment();
+ Bundle args = new Bundle();
+ args.putString(SYSTEM_LOCALE, defaultLocale.toLanguageTag());
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ binding = FLanguageSelectionBinding.inflate(inflater, container, false);
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ setActionBarSubtitle(this, R.string.select_language);
+
+ initRecyclerView(Arrays.asList(getResources().getStringArray(R.array.supported_languages)));
+ }
+
+ private static void customizeSelectionItem(VSelectTextListItemBinding binding) {
+ binding.title.setVisibility(View.GONE);
+ binding.bridgeImage.setVisibility(View.GONE);
+ binding.quality.setVisibility(View.GONE);
+ }
+
+ private void initRecyclerView(List<String> supportedLanguages) {
+ Locale defaultLocale = AppCompatDelegate.getApplicationLocales().get(0);
+ if (defaultLocale == null) {
+ defaultLocale = LocaleListCompat.getDefault().get(0);
+ }
+ // NOTE: Sort the supported languages by their display names.
+ // This would make updating supported languages easier as we don't have to tip toe around the order
+ Collections.sort(supportedLanguages, (lang1, lang2) -> {
+ String displayName1 = Locale.forLanguageTag(lang1).getDisplayName(Locale.ENGLISH);
+ String displayName2 = Locale.forLanguageTag(lang2).getDisplayName(Locale.ENGLISH);
+ return displayName1.compareTo(displayName2);
+ });
+ binding.languages.setAdapter(
+ new LanguageSelectionAdapter(supportedLanguages, this::updateLocale, defaultLocale)
+ );
+ binding.languages.setLayoutManager(new LinearLayoutManager(getContext()));
+ }
+
+ public static Locale getCurrentLocale() {
+ Locale defaultLocale = AppCompatDelegate.getApplicationLocales().get(0);
+ if (defaultLocale == null) {
+ defaultLocale = LocaleListCompat.getDefault().get(0);
+ }
+ return defaultLocale;
+ }
+
+ /**
+ * Update the locale of the application
+ *
+ * @param languageTag the language tag to set the locale to
+ */
+ private void updateLocale(String languageTag) {
+ if (languageTag.isEmpty()) {
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList());
+ } else {
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(languageTag));
+ }
+ }
+
+ /**
+ * Adapter for the language selection recycler view.
+ */
+ static class LanguageSelectionAdapter extends RecyclerView.Adapter<LanguageSelectionAdapter.LanguageViewHolder> {
+
+ private final List<String> languages;
+ private final LanguageClickListener clickListener;
+ private final Locale selectedLocale;
+
+ public LanguageSelectionAdapter(List<String> languages, LanguageClickListener clickListener, Locale defaultLocale) {
+ this.languages = languages;
+ this.clickListener = clickListener;
+ this.selectedLocale = defaultLocale;
+ }
+
+ @NonNull
+ @Override
+ public LanguageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ VSelectTextListItemBinding binding = VSelectTextListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
+ return new LanguageViewHolder(binding);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull LanguageViewHolder holder, int position) {
+ String languageTag = languages.get(position);
+ holder.languageName.setText(Locale.forLanguageTag(languageTag).getDisplayName(Locale.ENGLISH));
+ if (languages.contains(selectedLocale.toLanguageTag())) {
+ holder.selected.setChecked(selectedLocale.toLanguageTag().equals(languageTag));
+ } else {
+ holder.selected.setChecked(selectedLocale.getLanguage().equals(languageTag));
+ }
+ holder.itemView.setOnClickListener(v -> clickListener.onLanguageClick(languageTag));
+ }
+
+ @Override
+ public int getItemCount() {
+ return languages.size();
+ }
+
+ /**
+ * View holder for the language item
+ */
+ static class LanguageViewHolder extends RecyclerView.ViewHolder {
+ TextView languageName;
+ SimpleCheckBox selected;
+
+ public LanguageViewHolder(@NonNull VSelectTextListItemBinding binding) {
+ super(binding.getRoot());
+ languageName = binding.location;
+ selected = binding.selected;
+ customizeSelectionItem(binding);
+ }
+ }
+ }
+
+
+ /**
+ * Interface for the language click listener
+ */
+ interface LanguageClickListener {
+ void onLanguageClick(String languageTag);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java
index 56b7259e..c165d19b 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java
@@ -43,6 +43,7 @@ import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
+import androidx.core.os.BundleCompat;
import androidx.fragment.app.ListFragment;
import java.text.SimpleDateFormat;
@@ -300,7 +301,7 @@ public class LogFragment extends ListFragment implements StateListener, SeekBar.
// We have been called
if (msg.what == MESSAGE_NEWLOG) {
- LogItem logMessage = msg.getData().getParcelable("logmessage");
+ LogItem logMessage = BundleCompat.getParcelable(msg.getData(), "logmessage", LogItem.class);
if (addLogMessage(logMessage))
for (DataSetObserver observer : observers) {
observer.onChanged();
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java
index 3dbdbe64..ca84d330 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java
@@ -37,6 +37,7 @@ import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
+import androidx.core.os.BundleCompat;
import androidx.fragment.app.DialogFragment;
import org.json.JSONObject;
@@ -186,7 +187,7 @@ public class MainActivityErrorDialog extends DialogFragment {
return;
}
if (savedInstanceState.containsKey(KEY_PROVIDER)) {
- this.provider = savedInstanceState.getParcelable(KEY_PROVIDER);
+ this.provider = BundleCompat.getParcelable(savedInstanceState, KEY_PROVIDER, Provider.class);
}
if (savedInstanceState.containsKey(KEY_REASON_TO_FAIL)) {
this.reasonToFail = savedInstanceState.getString(KEY_REASON_TO_FAIL);
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java
index cbab1d32..41e102bb 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java
@@ -46,15 +46,19 @@ import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.graphics.drawable.DrawerArrowDrawable;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
+import androidx.core.os.LocaleListCompat;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
+import java.util.Locale;
+import java.util.Map;
import se.leap.bitmaskclient.BuildConfig;
import se.leap.bitmaskclient.R;
@@ -211,6 +215,7 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen
initSwitchProviderEntry();
initSaveBatteryEntry();
initManualGatewayEntry();
+ initLanguageSettingsEntry();
initAdvancedSettingsEntry();
initDonateEntry();
initLogEntry();
@@ -314,6 +319,26 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen
});
}
+
+ private void initLanguageSettingsEntry() {
+ IconTextEntry languageSwitcher = drawerView.findViewById(R.id.language_switcher);
+
+ Locale currentLocale = LanguageSelectionFragment.getCurrentLocale();
+ languageSwitcher.setSubtitle(currentLocale.getDisplayName(Locale.ENGLISH));
+
+ languageSwitcher.setOnClickListener(v -> {
+ FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
+ closeDrawer();
+ Fragment current = fragmentManager.findFragmentByTag(MainActivity.TAG);
+ if (current instanceof LanguageSelectionFragment) {
+ return;
+ }
+ Fragment fragment = LanguageSelectionFragment.newInstance(Locale.getDefault());
+ setDrawerToggleColor(drawerView.getContext(), ContextCompat.getColor(drawerView.getContext(), R.color.colorActionBarTitleFont));
+ fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
+ });
+ }
+
private void initDonateEntry() {
if (ENABLE_DONATION) {
IconTextEntry donate = drawerView.findViewById(R.id.donate);
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/ObfuscationProxyDialog.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/ObfuscationProxyDialog.java
index 7d12ca70..2e4eec8a 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/ObfuscationProxyDialog.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/ObfuscationProxyDialog.java
@@ -2,11 +2,16 @@ package se.leap.bitmaskclient.base.fragments;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
+import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.KCP;
+import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.QUIC;
+import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.TCP;
import android.app.Dialog;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -14,10 +19,10 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDialogFragment;
import androidx.appcompat.widget.AppCompatButton;
import androidx.appcompat.widget.AppCompatEditText;
+import androidx.appcompat.widget.AppCompatSpinner;
import se.leap.bitmaskclient.base.utils.BuildConfigHelper;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
-import se.leap.bitmaskclient.base.views.IconSwitchEntry;
import se.leap.bitmaskclient.databinding.DObfuscationProxyBinding;
import se.leap.bitmaskclient.eip.GatewaysManager;
@@ -30,7 +35,9 @@ public class ObfuscationProxyDialog extends AppCompatDialogFragment {
AppCompatButton saveButton;
AppCompatButton useDefaultsButton;
AppCompatButton cancelButton;
- IconSwitchEntry kcpSwitch;
+ AppCompatSpinner protocolSpinner;
+ private final String[] protocols = { TCP.toString(), KCP.toString(), QUIC.toString() };
+
@NonNull
@Override
@@ -46,15 +53,29 @@ public class ObfuscationProxyDialog extends AppCompatDialogFragment {
saveButton = binding.buttonSave;
useDefaultsButton = binding.buttonDefaults;
cancelButton = binding.buttonCancel;
- kcpSwitch = binding.kcpSwitch;
+ protocolSpinner = binding.protocolSpinner;
ipField.setText(PreferenceHelper.getObfuscationPinningIP());
portField.setText(PreferenceHelper.getObfuscationPinningPort());
certificateField.setText(PreferenceHelper.getObfuscationPinningCert());
- kcpSwitch.setChecked(PreferenceHelper.getObfuscationPinningKCP());
GatewaysManager gatewaysManager = new GatewaysManager(getContext());
+
+ ArrayAdapter<String> adapter = new ArrayAdapter<>(binding.getRoot().getContext(), android.R.layout.simple_spinner_item, protocols);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ protocolSpinner.setAdapter(adapter);
+
+ protocolSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ PreferenceHelper.setObfuscationPinningProtocol(protocols[position]);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {}
+ });
+
saveButton.setOnClickListener(v -> {
String ip = TextUtils.isEmpty(ipField.getText()) ? null : ipField.getText().toString();
PreferenceHelper.setObfuscationPinningIP(ip);
@@ -62,7 +83,6 @@ public class ObfuscationProxyDialog extends AppCompatDialogFragment {
PreferenceHelper.setObfuscationPinningPort(port);
String cert = TextUtils.isEmpty(certificateField.getText()) ? null : certificateField.getText().toString();
PreferenceHelper.setObfuscationPinningCert(cert);
- PreferenceHelper.setObfuscationPinningKCP(kcpSwitch.isChecked());
PreferenceHelper.setUseObfuscationPinning(ip != null && port != null && cert != null);
PreferenceHelper.setObfuscationPinningGatewayLocation(gatewaysManager.getLocationNameForIP(ip, v.getContext()));
dismiss();
@@ -73,7 +93,7 @@ public class ObfuscationProxyDialog extends AppCompatDialogFragment {
ipField.setText(BuildConfigHelper.obfsvpnIP());
portField.setText(BuildConfigHelper.obfsvpnPort());
certificateField.setText(BuildConfigHelper.obfsvpnCert());
- kcpSwitch.setChecked(BuildConfigHelper.useKcp());
+ protocolSpinner.setSelection(getIndexForProtocol(BuildConfigHelper.obfsvpnTransportProtocol()));
});
cancelButton.setOnClickListener(v -> {
@@ -85,6 +105,15 @@ public class ObfuscationProxyDialog extends AppCompatDialogFragment {
return builder.create();
}
+ private int getIndexForProtocol(String protocol) {
+ for (int i = 0; i < protocols.length; i++) {
+ if (protocols[i].equals(protocol)) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
@Override
public void onDestroyView() {
super.onDestroyView();
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/SettingsFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/SettingsFragment.java
index d7b62de2..d4d72812 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/SettingsFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/SettingsFragment.java
@@ -3,12 +3,12 @@ package se.leap.bitmaskclient.base.fragments;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static se.leap.bitmaskclient.R.string.advanced_settings;
+import static se.leap.bitmaskclient.base.fragments.CensorshipCircumventionFragment.TUNNELING_AUTOMATICALLY;
import static se.leap.bitmaskclient.base.models.Constants.GATEWAY_PINNING;
import static se.leap.bitmaskclient.base.models.Constants.PREFER_UDP;
import static se.leap.bitmaskclient.base.models.Constants.USE_BRIDGES;
import static se.leap.bitmaskclient.base.models.Constants.USE_IPv6_FIREWALL;
import static se.leap.bitmaskclient.base.models.Constants.USE_OBFUSCATION_PINNING;
-import static se.leap.bitmaskclient.base.utils.BuildConfigHelper.useObfsVpn;
import static se.leap.bitmaskclient.base.utils.ConfigHelper.isCalyxOSWithTetheringSupport;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.allowExperimentalTransports;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getExcludedApps;
@@ -18,11 +18,16 @@ import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseBridges;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseSnowflake;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.hasSnowflakePrefs;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.preferUDP;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.resetSnowflakeSettings;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.setAllowExperimentalTransports;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.setUseObfuscationPinning;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.setUsePortHopping;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.setUseTunnel;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useBridges;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useManualDiscoverySettings;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useObfuscationPinning;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useSnowflake;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useManualBridgeSettings;
import static se.leap.bitmaskclient.base.utils.ViewHelper.setActionBarSubtitle;
import android.app.AlertDialog;
@@ -36,10 +41,13 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
+import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.appcompat.widget.SwitchCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
@@ -79,8 +87,8 @@ public class SettingsFragment extends Fragment implements SharedPreferences.OnSh
initAlwaysOnVpnEntry(view);
initExcludeAppsEntry(view);
initPreferUDPEntry(view);
- initUseBridgesEntry(view);
- initUseSnowflakeEntry(view);
+ initAutomaticCircumventionEntry(view);
+ initManualCircumventionEntry(view);
initFirewallEntry(view);
initTetheringEntry(view);
initGatewayPinningEntry(view);
@@ -90,48 +98,91 @@ public class SettingsFragment extends Fragment implements SharedPreferences.OnSh
return view;
}
- @Override
- public void onDestroy() {
- PreferenceHelper.unregisterOnSharedPreferenceChangeListener(this);
- super.onDestroy();
- }
+ private void initAutomaticCircumventionEntry(View rootView) {
+ IconSwitchEntry automaticCircumvention = rootView.findViewById(R.id.bridge_automatic_switch);
+ automaticCircumvention.setChecked(getUseBridges() && !useManualBridgeSettings());
+ automaticCircumvention.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (!buttonView.isPressed()) {
+ return;
+ }
- private void initUseBridgesEntry(View rootView) {
- IconSwitchEntry useBridges = rootView.findViewById(R.id.bridges_switch);
- if (ProviderObservable.getInstance().getCurrentProvider().supportsPluggableTransports()) {
- useBridges.setVisibility(VISIBLE);
- useBridges.setChecked(getUseBridges());
- useBridges.setOnCheckedChangeListener((buttonView, isChecked) -> {
- if (!buttonView.isPressed()) {
- return;
- }
+ if (isChecked) {
+ resetSnowflakeSettings();
+ setUseTunnel(TUNNELING_AUTOMATICALLY);
+ setUsePortHopping(false);
+ } else {
+ useSnowflake(false);
+ }
+ if (ProviderObservable.getInstance().getCurrentProvider().supportsPluggableTransports()) {
useBridges(isChecked);
if (VpnStatus.isVPNActive()) {
EipCommand.startVPN(getContext(), false);
Toast.makeText(getContext(), R.string.reconnecting, Toast.LENGTH_LONG).show();
}
- });
- //We check the UI state of the useUdpEntry here as well, in order to avoid a situation
- //where both entries are disabled, because both preferences are enabled.
- //bridges can be enabled not only from here but also from error handling
- boolean useUDP = getPreferUDP() && useUdpEntry.isEnabled();
- useBridges.setEnabled(!useUDP);
- useBridges.setSubtitle(getString(useUDP ? R.string.disabled_while_udp_on : R.string.nav_drawer_subtitle_obfuscated_connection));
- } else {
- useBridges.setVisibility(GONE);
- }
+ }
+ });
+
+ //We check the UI state of the useUdpEntry here as well, in order to avoid a situation
+ //where both entries are disabled, because both preferences are enabled.
+ //bridges can be enabled not only from here but also from error handling
+ boolean useUDP = getPreferUDP() && useUdpEntry.isEnabled();
+ automaticCircumvention.setEnabled(!useUDP);
+ automaticCircumvention.setSubtitle(getString(useUDP ? R.string.disabled_while_udp_on : R.string.automatic_bridge_description));
}
- private void initUseSnowflakeEntry(View rootView) {
- IconSwitchEntry useSnowflake = rootView.findViewById(R.id.snowflake_switch);
- useSnowflake.setVisibility(VISIBLE);
- useSnowflake.setChecked(hasSnowflakePrefs() && getUseSnowflake());
- useSnowflake.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ private void initManualCircumventionEntry(View rootView) {
+ LinearLayout manualConfigRoot = rootView.findViewById(R.id.bridge_manual_switch_entry);
+ manualConfigRoot.setVisibility(ProviderObservable.getInstance().getCurrentProvider().supportsPluggableTransports() ? VISIBLE : GONE);
+ IconTextEntry manualConfiguration = rootView.findViewById(R.id.bridge_manual_switch);
+ SwitchCompat manualConfigurationSwitch = rootView.findViewById(R.id.bridge_manual_switch_control);
+ boolean useManualCircumventionSettings = useManualBridgeSettings() || useManualDiscoverySettings();
+ manualConfigurationSwitch.setChecked(useManualCircumventionSettings);
+ manualConfigurationSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (!buttonView.isPressed()) {
return;
}
- useSnowflake(isChecked);
+ resetManualConfig();
+ if (!useManualCircumventionSettings){
+ openManualConfigurationFragment();
+ }
});
+ manualConfiguration.setOnClickListener((buttonView) -> openManualConfigurationFragment());
+
+ //We check the UI state of the useUdpEntry here as well, in order to avoid a situation
+ //where both entries are disabled, because both preferences are enabled.
+ //bridges can be enabled not only from here but also from error handling
+ boolean useUDP = getPreferUDP() && useUdpEntry.isEnabled();
+ manualConfiguration.setEnabled(!useUDP);
+ manualConfigurationSwitch.setVisibility(useUDP ? GONE : VISIBLE);
+ manualConfiguration.setSubtitle(getString(useUDP ? R.string.disabled_while_udp_on : R.string.manual_bridge_description));
+ }
+
+ private void resetManualConfig() {
+ useSnowflake(false);
+ setUseTunnel(TUNNELING_AUTOMATICALLY);
+ setUsePortHopping(false);
+ useBridges(false);
+ if (VpnStatus.isVPNActive()) {
+ EipCommand.startVPN(getContext(), false);
+ Toast.makeText(getContext(), R.string.reconnecting, Toast.LENGTH_LONG).show();
+ }
+ View rootView = getView();
+ if (rootView == null) {
+ return;
+ }
+ initAutomaticCircumventionEntry(rootView);
+ }
+
+ private void openManualConfigurationFragment() {
+ FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
+ Fragment fragment = CensorshipCircumventionFragment.newInstance();
+ fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
+ }
+
+ @Override
+ public void onDestroy() {
+ PreferenceHelper.unregisterOnSharedPreferenceChangeListener(this);
+ super.onDestroy();
}
private void initAlwaysOnVpnEntry(View rootView) {
@@ -225,7 +276,7 @@ public class SettingsFragment extends Fragment implements SharedPreferences.OnSh
private void initGatewayPinningEntry(View rootView) {
IconTextEntry gatewayPinning = rootView.findViewById(R.id.gateway_pinning);
- if (!BuildConfig.BUILD_TYPE.equals("debug")) {
+ if (!BuildConfig.DEBUG_MODE) {
gatewayPinning.setVisibility(GONE);
return;
}
@@ -261,7 +312,7 @@ public class SettingsFragment extends Fragment implements SharedPreferences.OnSh
public void initObfuscationPinningEntry(View rootView) {
IconSwitchEntry obfuscationPinning = rootView.findViewById(R.id.obfuscation_proxy_pinning);
- if (!BuildConfig.BUILD_TYPE.equals("debug") || !useObfsVpn()) {
+ if (!BuildConfig.DEBUG_MODE) {
obfuscationPinning.setVisibility(GONE);
return;
}
@@ -302,7 +353,7 @@ public class SettingsFragment extends Fragment implements SharedPreferences.OnSh
public void initExperimentalTransportsEntry(View rootView) {
IconSwitchEntry experimentalTransports = rootView.findViewById(R.id.experimental_transports);
- if (useObfsVpn() && ProviderObservable.getInstance().getCurrentProvider().supportsExperimentalPluggableTransports()) {
+ if (ProviderObservable.getInstance().getCurrentProvider().supportsExperimentalPluggableTransports()) {
experimentalTransports.setVisibility(VISIBLE);
experimentalTransports.setChecked(allowExperimentalTransports());
experimentalTransports.setOnCheckedChangeListener((buttonView, isChecked) -> {
@@ -351,8 +402,9 @@ public class SettingsFragment extends Fragment implements SharedPreferences.OnSh
return;
}
if (key.equals(USE_BRIDGES) || key.equals(PREFER_UDP)) {
- initUseBridgesEntry(rootView);
initPreferUDPEntry(rootView);
+ initManualCircumventionEntry(rootView);
+ initAutomaticCircumventionEntry(rootView);
} else if (key.equals(USE_IPv6_FIREWALL)) {
initFirewallEntry(getView());
} else if (key.equals(GATEWAY_PINNING)) {
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
index 754491f8..b8849c4d 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
@@ -42,6 +42,8 @@ public interface Constants {
String RESTART_ON_UPDATE = "restart_on_update";
String LAST_UPDATE_CHECK = "last_update_check";
String PREFERRED_CITY = "preferred_city";
+ // ATTENTION: this key is also used in bitmask-core for persistence
+ String COUNTRYCODE = "COUNTRYCODE";
String USE_SNOWFLAKE = "use_snowflake";
String PREFER_UDP = "prefer_UDP";
String GATEWAY_PINNING = "gateway_pinning";
@@ -51,9 +53,12 @@ public interface Constants {
String OBFUSCATION_PINNING_PORT = "obfuscation_pinning_port";
String OBFUSCATION_PINNING_CERT = "obfuscation_pinning_cert";
String OBFUSCATION_PINNING_KCP = "obfuscation_pinning_udp";
+ String OBFUSCATION_PINNING_PROTOCOL = "obfuscation_pinning_protocol";
String OBFUSCATION_PINNING_LOCATION = "obfuscation_pinning_location";
String USE_SYSTEM_PROXY = "usesystemproxy";
String CUSTOM_PROVIDER_DOMAINS = "custom_provider_domains";
+ String USE_PORT_HOPPING = "use_port_hopping";
+ String USE_TUNNEL = "tunnel";
//////////////////////////////////////////////
@@ -122,6 +127,10 @@ public interface Constants {
String PROVIDER_MOTD_HASHES = "Constants.PROVIDER_MOTD_HASHES";
String PROVIDER_MOTD_LAST_SEEN = "Constants.PROVIDER_MOTD_LAST_SEEN";
String PROVIDER_MOTD_LAST_UPDATED = "Constants.PROVIDER_MOTD_LAST_UPDATED";
+ String PROVIDER_MODELS_PROVIDER = "Constants.PROVIDER_MODELS_PROVIDER";
+ String PROVIDER_MODELS_EIPSERVICE = "Constants.PROVIDER_MDOELS_EIPSERVICE";
+ String PROVIDER_MODELS_GATEWAYS = "Constants.PROVIDER_MODELS_GATEWAYS";
+ String PROVIDER_MODELS_BRIDGES = "Constants.PROVIDER_MODELS_BRIDGES";
////////////////////////////////////////////////
// PRESHIPPED PROVIDER CONFIG
@@ -184,6 +193,7 @@ public interface Constants {
String UDP = "udp";
String TCP = "tcp";
String KCP = "kcp";
+ String QUIC = "quic";
String CAPABILITIES = "capabilities";
String TRANSPORT = "transport";
String TYPE = "type";
@@ -194,6 +204,10 @@ public interface Constants {
String ENDPOINTS = "endpoints";
String PORT_SEED = "port_seed";
String PORT_COUNT = "port_count";
+ String HOP_JITTER = "hop_jitter";
+ String MIN_HOP_PORT = "min_hop_port";
+ String MAX_HOP_PORT = "max_hop_port";
+ String MIN_HOP_SECONDS = "min_hop_seconds";
String EXPERIMENTAL = "experimental";
String VERSION = "version";
String NAME = "name";
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java b/app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java
index 8b78c966..1771f0b0 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java
@@ -5,6 +5,7 @@ public interface FeatureVersionCode {
int GEOIP_SERVICE = 148;
int CALYX_PROVIDER_LILYPAD_UPDATE = 165000;
int RISEUP_PROVIDER_LILYPAD_UPDATE = 165000;
+ int RISEUP_PROVIDER_LILYPAD_UPDATE_v2 = 172000;
int ENCRYPTED_SHARED_PREFS = 170000;
int NOTIFICATION_PREMISSION_API_UPDATE = 170000;
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Introducer.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Introducer.java
new file mode 100644
index 00000000..e3175010
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Introducer.java
@@ -0,0 +1,132 @@
+package se.leap.bitmaskclient.base.models;
+
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.io.UnsupportedEncodingException;
+import java.net.IDN;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+import java.util.Locale;
+
+public class Introducer implements Parcelable {
+ private final String type;
+ private final String address;
+ private final String certificate;
+ private final String fullyQualifiedDomainName;
+ private final boolean kcpEnabled;
+ private final String auth;
+
+ public Introducer(String type, String address, String certificate, String fullyQualifiedDomainName, boolean kcpEnabled, String auth) {
+ this.type = type;
+ this.address = address;
+ this.certificate = certificate;
+ this.fullyQualifiedDomainName = fullyQualifiedDomainName;
+ this.kcpEnabled = kcpEnabled;
+ this.auth = auth;
+ }
+
+ protected Introducer(Parcel in) {
+ type = in.readString();
+ address = in.readString();
+ certificate = in.readString();
+ fullyQualifiedDomainName = in.readString();
+ kcpEnabled = in.readByte() != 0;
+ auth = in.readString();
+ }
+
+ public String getFullyQualifiedDomainName() {
+ return fullyQualifiedDomainName;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(type);
+ dest.writeString(address);
+ dest.writeString(certificate);
+ dest.writeString(fullyQualifiedDomainName);
+ dest.writeByte((byte) (kcpEnabled ? 1 : 0));
+ dest.writeString(auth);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator<Introducer> CREATOR = new Creator<>() {
+ @Override
+ public Introducer createFromParcel(Parcel in) {
+ return new Introducer(in);
+ }
+
+ @Override
+ public Introducer[] newArray(int size) {
+ return new Introducer[size];
+ }
+ };
+
+ public boolean validate() {
+ if (!"obfsvpnintro".equals(type)) {
+ throw new IllegalArgumentException("Unknown type: " + type);
+ }
+ if (!address.contains(":") || address.split(":").length != 2) {
+ throw new IllegalArgumentException("Expected address in format ipaddr:port");
+ }
+ if (certificate.length() != 70) {
+ throw new IllegalArgumentException("Wrong certificate length: " + certificate.length());
+ }
+ if (!"localhost".equals(fullyQualifiedDomainName) && fullyQualifiedDomainName.split("\\.").length < 2) {
+ throw new IllegalArgumentException("Expected a FQDN, got: " + fullyQualifiedDomainName);
+ }
+
+ if (auth == null || auth.isEmpty()) {
+ throw new IllegalArgumentException("Auth token is missing");
+ }
+ return true;
+ }
+
+ public static Introducer fromUrl(String introducerUrl) throws URISyntaxException, IllegalArgumentException {
+ Uri uri = Uri.parse(introducerUrl);
+ String fqdn = uri.getQueryParameter("fqdn");
+ if (fqdn == null || fqdn.isEmpty()) {
+ throw new IllegalArgumentException("FQDN not found in the introducer URL");
+ }
+
+ if (!isAscii(fqdn)) {
+ throw new IllegalArgumentException("FQDN is not ASCII: " + fqdn);
+ }
+
+ boolean kcp = "1".equals(uri.getQueryParameter( "kcp"));
+
+ String cert = uri.getQueryParameter( "cert");
+ if (cert == null || cert.isEmpty()) {
+ throw new IllegalArgumentException("Cert not found in the introducer URL");
+ }
+
+ String auth = uri.getQueryParameter( "auth");
+ if (auth == null || auth.isEmpty()) {
+ throw new IllegalArgumentException("Authentication token not found in the introducer URL");
+ }
+ return new Introducer(uri.getScheme(), uri.getAuthority(), cert, fqdn, kcp, auth);
+ }
+
+ public String getAuthToken() {
+ return auth;
+ }
+
+ private static boolean isAscii(String fqdn) {
+ try {
+ String asciiFQDN = IDN.toASCII(fqdn, IDN.USE_STD3_ASCII_RULES);
+ return fqdn.equals(asciiFQDN);
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ public String toUrl() throws UnsupportedEncodingException {
+ return String.format(Locale.US, "%s://%s?fqdn=%s&kcp=%d&cert=%s&auth=%s", type, address, URLEncoder.encode(fullyQualifiedDomainName, "UTF-8"), kcpEnabled ? 1 : 0, URLEncoder.encode(certificate, "UTF-8"), URLEncoder.encode(auth, "UTF-8"));
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java
index cb9bd520..b4ec23e6 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java
@@ -17,6 +17,7 @@
package se.leap.bitmaskclient.base.models;
import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.KCP;
+import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.QUIC;
import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.TCP;
import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_HOP;
@@ -28,8 +29,9 @@ import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_ALLOWED_REGIS
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_ALLOW_ANONYMOUS;
import static se.leap.bitmaskclient.base.models.Constants.TRANSPORT;
import static se.leap.bitmaskclient.base.models.Constants.TYPE;
-import static se.leap.bitmaskclient.base.utils.BuildConfigHelper.useObfsVpn;
-import static se.leap.bitmaskclient.base.utils.RSAHelper.parseRsaKeyFromString;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.isDomainName;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.isNetworkUrl;
+import static se.leap.bitmaskclient.base.utils.PrivateKeyHelper.parsePrivateKeyFromString;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
import android.os.Parcel;
@@ -38,23 +40,33 @@ import android.os.Parcelable;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.MalformedURLException;
+import java.net.URISyntaxException;
import java.net.URL;
-import java.security.interfaces.RSAPrivateKey;
+import java.security.PrivateKey;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
+import java.util.Objects;
import java.util.Set;
import de.blinkt.openvpn.core.connection.Connection.TransportProtocol;
import de.blinkt.openvpn.core.connection.Connection.TransportType;
+import io.swagger.client.JSON;
+import io.swagger.client.model.ModelsBridge;
+import io.swagger.client.model.ModelsEIPService;
+import io.swagger.client.model.ModelsGateway;
+import io.swagger.client.model.ModelsProvider;
import motd.IStringCollection;
import motd.Motd;
+import se.leap.bitmaskclient.BuildConfig;
/**
* @author Sean Leonard <meanderingcode@aetherislands.net>
@@ -69,25 +81,30 @@ public final class Provider implements Parcelable {
private JSONObject eipServiceJson = new JSONObject();
private JSONObject geoIpJson = new JSONObject();
private JSONObject motdJson = new JSONObject();
- private DefaultedURL mainUrl = new DefaultedURL();
- private DefaultedURL apiUrl = new DefaultedURL();
- private DefaultedURL geoipUrl = new DefaultedURL();
- private DefaultedURL motdUrl = new DefaultedURL();
+ private String mainUrl = "";
+ private String apiUrl = "";
+ private String geoipUrl = "";
+ private String motdUrl = "";
+ private ModelsEIPService modelsEIPService = null;
+ private ModelsProvider modelsProvider = null;
+ private ModelsGateway[] modelsGateways = null;
+ private ModelsBridge[] modelsBridges = null;
private String domain = "";
private String providerIp = ""; // ip of the provider main url
private String providerApiIp = ""; // ip of the provider api url
private String certificatePin = "";
private String certificatePinEncoding = "";
private String caCert = "";
- private String apiVersion = "";
- private String privateKey = "";
-
- private transient RSAPrivateKey rsaPrivateKey = null;
+ private int apiVersion = 3;
+ private int[] apiVersions = new int[0];
+ private String privateKeyString = "";
+ private transient PrivateKey privateKey = null;
private String vpnCertificate = "";
private long lastEipServiceUpdate = 0L;
private long lastGeoIpUpdate = 0L;
private long lastMotdUpdate = 0L;
private long lastMotdSeen = 0L;
+ private Introducer introducer = null;
private Set<String> lastMotdSeenHashes = new HashSet<>();
private boolean shouldUpdateVpnCertificate;
@@ -97,6 +114,7 @@ public final class Provider implements Parcelable {
final public static String
API_URL = "api_uri",
API_VERSION = "api_version",
+ API_VERSIONS = "api_versions",
ALLOW_REGISTRATION = "allow_registration",
API_RETURN_SERIAL = "serial",
SERVICE = "service",
@@ -117,37 +135,36 @@ public final class Provider implements Parcelable {
public Provider() { }
+ public Provider(Introducer introducer) {
+ this("https://" + introducer.getFullyQualifiedDomainName());
+ this.introducer = introducer;
+ }
+
public Provider(String mainUrl) {
- this(mainUrl, null);
+ this(mainUrl, null);
+ domain = getHostFromUrl(mainUrl);
}
public Provider(String mainUrl, String geoipUrl) {
- try {
- this.mainUrl.setUrl(new URL(mainUrl));
- } catch (MalformedURLException e) {
- this.mainUrl = new DefaultedURL();
- }
+ setMainUrl(mainUrl);
setGeoipUrl(geoipUrl);
+ domain = getHostFromUrl(mainUrl);
}
- public static Provider createCustomProvider(String mainUrl, String domain) {
+ public static Provider createCustomProvider(String mainUrl, String domain, Introducer introducer) {
Provider p = new Provider(mainUrl);
p.domain = domain;
+ p.introducer = introducer;
return p;
}
public Provider(String mainUrl, String geoipUrl, String motdUrl, String providerIp, String providerApiIp) {
- try {
- this.mainUrl.setUrl(new URL(mainUrl));
- if (providerIp != null) {
- this.providerIp = providerIp;
- }
- if (providerApiIp != null) {
- this.providerApiIp = providerApiIp;
- }
- } catch (MalformedURLException e) {
- e.printStackTrace();
- return;
+ setMainUrl(mainUrl);
+ if (providerIp != null) {
+ this.providerIp = providerIp;
+ }
+ if (providerApiIp != null) {
+ this.providerApiIp = providerApiIp;
}
setGeoipUrl(geoipUrl);
setMotdUrl(motdUrl);
@@ -179,64 +196,195 @@ public final class Provider implements Parcelable {
}
};
+ public void setBridges(String bridgesJson) {
+ if (bridgesJson == null) {
+ this.modelsBridges = null;
+ return;
+ }
+ try {
+ this.modelsBridges = JSON.createGson().create().fromJson(bridgesJson, ModelsBridge[].class);
+ } catch (JsonSyntaxException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public ModelsBridge[] getBridges() {
+ return this.modelsBridges;
+ }
+
+ public String getBridgesJson() {
+ return getJsonString(modelsBridges);
+ }
+
+ public void setGateways(String gatewaysJson) {
+ if (gatewaysJson == null) {
+ this.modelsGateways = null;
+ return;
+ }
+ try {
+ this.modelsGateways = JSON.createGson().create().fromJson(gatewaysJson, ModelsGateway[].class);
+ } catch (JsonSyntaxException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public ModelsGateway[] getGateways() {
+ return modelsGateways;
+ }
+
+ public String getGatewaysJson() {
+ return getJsonString(modelsGateways);
+ }
+
+ public void setService(String serviceJson) {
+ if (serviceJson == null) {
+ this.modelsEIPService = null;
+ return;
+ }
+ try {
+ this.modelsEIPService = JSON.createGson().create().fromJson(serviceJson, ModelsEIPService.class);
+ } catch (JsonSyntaxException e) {
+ e.printStackTrace();
+ }
+ }
+ public ModelsEIPService getService() {
+ return this.modelsEIPService;
+ }
+
+ public String getServiceJson() {
+ return getJsonString(modelsEIPService);
+ }
+
+ public void setModelsProvider(String json) {
+ if (json == null) {
+ this.modelsProvider = null;
+ return;
+ }
+ try {
+ this.modelsProvider = JSON.createGson().create().fromJson(json, ModelsProvider.class);
+ } catch (JsonSyntaxException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public String getModelsProviderJson() {
+ return getJsonString(modelsProvider);
+ }
+
+ private String getJsonString(Object model) {
+ if (model == null) {
+ return null;
+ }
+ try {
+ return JSON.createGson().create().toJson(model);
+ } catch (JsonSyntaxException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
public boolean isConfigured() {
- return !mainUrl.isDefault() &&
- !apiUrl.isDefault() &&
- hasCaCert() &&
- hasDefinition() &&
- hasVpnCertificate() &&
- hasEIP() &&
- hasPrivateKey();
+ if (apiVersion < 5) {
+ return !mainUrl.isEmpty() &&
+ !apiUrl.isEmpty() &&
+ hasCaCert() &&
+ hasDefinition() &&
+ hasVpnCertificate() &&
+ hasEIP() &&
+ hasPrivateKey();
+ } else {
+ return !mainUrl.isEmpty() &&
+ modelsProvider != null &&
+ modelsEIPService != null &&
+ modelsGateways != null &&
+ hasVpnCertificate() &&
+ hasPrivateKey();
+ }
}
public boolean supportsPluggableTransports() {
- if (useObfsVpn()) {
- return supportsTransports(new Pair[]{new Pair<>(OBFS4, TCP), new Pair<>(OBFS4, KCP), new Pair<>(OBFS4_HOP, TCP), new Pair<>(OBFS4_HOP, KCP)});
- }
- return supportsTransports(new Pair[]{new Pair<>(OBFS4, TCP)});
+ return supportsTransports(new Pair[]{new Pair<>(OBFS4, TCP), new Pair<>(OBFS4, KCP), new Pair<>(OBFS4, QUIC), new Pair<>(OBFS4_HOP, TCP), new Pair<>(OBFS4_HOP, KCP), new Pair<>(OBFS4_HOP, QUIC)});
}
public boolean supportsExperimentalPluggableTransports() {
- return supportsTransports(new Pair[]{new Pair<>(OBFS4, KCP), new Pair<>(OBFS4_HOP, TCP), new Pair<>(OBFS4_HOP, KCP)});
+ return supportsTransports(new Pair[]{new Pair<>(OBFS4, KCP), new Pair<>(OBFS4_HOP, TCP), new Pair<>(OBFS4_HOP, KCP), new Pair<>(OBFS4, QUIC), new Pair<>(OBFS4_HOP, QUIC)});
+ }
+
+
+ public boolean supportsObfs4() {
+ return supportsTransports(new Pair[]{new Pair<>(OBFS4, TCP)});
+ }
+
+ public boolean supportsObfs4Kcp() {
+ return supportsTransports(new Pair[]{new Pair<>(OBFS4, KCP)});
+ }
+
+ public boolean supportsObfs4Quic() {
+ return supportsTransports(new Pair[]{new Pair<>(OBFS4, QUIC)});
+ }
+
+ public boolean supportsObfs4Hop() {
+ return supportsTransports(new Pair[]{new Pair<>(OBFS4_HOP, KCP), new Pair<>(OBFS4_HOP, QUIC), new Pair<>(OBFS4_HOP, TCP)});
}
private boolean supportsTransports(Pair<TransportType, TransportProtocol>[] transportTypes) {
- 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++) {
- String supportedTransportType = transports.getJSONObject(j).getString(TYPE);
- JSONArray transportProtocols = transports.getJSONObject(j).getJSONArray(PROTOCOLS);
- for (Pair<TransportType, TransportProtocol> transportPair : transportTypes) {
- for (int k = 0; k < transportProtocols.length(); k++) {
- if (transportPair.first.toString().equals(supportedTransportType) &&
- transportPair.second.toString().equals(transportProtocols.getString(k))) {
- return true;
+ if (apiVersion < 5) {
+ 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++) {
+ String supportedTransportType = transports.getJSONObject(j).getString(TYPE);
+ JSONArray transportProtocols = transports.getJSONObject(j).getJSONArray(PROTOCOLS);
+ for (Pair<TransportType, TransportProtocol> transportPair : transportTypes) {
+ for (int k = 0; k < transportProtocols.length(); k++) {
+ if (transportPair.first.toString().equals(supportedTransportType) &&
+ transportPair.second.toString().equals(transportProtocols.getString(k))) {
+ return true;
+ }
}
}
}
}
+ } catch (Exception e) {
+ // ignore
+ }
+ } else {
+ if (modelsBridges == null) return false;
+ for (ModelsBridge bridge : modelsBridges) {
+ for (Pair<TransportType, TransportProtocol> transportPair : transportTypes) {
+ if (transportPair.first.toString().equals(bridge.getType()) &&
+ transportPair.second.toString().equals(bridge.getTransport())) {
+ return true;
+ }
+ }
}
- } catch (Exception e) {
- // ignore
}
+
return false;
}
public String getIpForHostname(String host) {
if (host != null) {
- if (host.equals(mainUrl.getUrl().getHost())) {
+ if (host.equals(getHostFromUrl(mainUrl))) {
return providerIp;
- } else if (host.equals(apiUrl.getUrl().getHost())) {
+ } else if (host.equals(getHostFromUrl(apiUrl))) {
return providerApiIp;
}
}
return "";
}
+ private String getHostFromUrl(String url) {
+ try {
+ return new URL(url).getHost();
+ } catch (MalformedURLException e) {
+ return "";
+ }
+ }
+
public String getProviderApiIp() {
return this.providerApiIp;
}
@@ -256,14 +404,21 @@ public final class Provider implements Parcelable {
}
public void setMainUrl(URL url) {
- mainUrl.setUrl(url);
+ mainUrl = url.toString();
}
public void setMainUrl(String url) {
try {
- mainUrl.setUrl(new URL(url));
+ if (isNetworkUrl(url)) {
+ this.mainUrl = new URL(url).toString();
+ } else if (isDomainName(url)){
+ this.mainUrl = new URL("https://" + url).toString();
+ } else {
+ this.mainUrl = "";
+ }
} catch (MalformedURLException e) {
e.printStackTrace();
+ this.mainUrl = "";
}
}
@@ -281,55 +436,54 @@ public final class Provider implements Parcelable {
}
public String getDomain() {
- return domain;
- }
-
- public String getMainUrlString() {
- return getMainUrl().toString();
+ if ((apiVersion < 5 && (domain == null || domain.isEmpty())) ||
+ (modelsProvider == null)) {
+ return getHostFromUrl(mainUrl);
+ }
+ if (apiVersion < 5) {
+ return domain;
+ }
+ return modelsProvider.getDomain();
}
- public DefaultedURL getMainUrl() {
+ public String getMainUrl() {
return mainUrl;
}
- protected DefaultedURL getApiUrl() {
- return apiUrl;
- }
-
- public DefaultedURL getGeoipUrl() {
+ public String getGeoipUrl() {
return geoipUrl;
}
public void setGeoipUrl(String url) {
try {
- this.geoipUrl.setUrl(new URL(url));
+ this.geoipUrl = new URL(url).toString();
} catch (MalformedURLException e) {
- this.geoipUrl = new DefaultedURL();
+ this.geoipUrl = "";
}
}
- public DefaultedURL getMotdUrl() {
+ public String getMotdUrl() {
return this.motdUrl;
}
public void setMotdUrl(String url) {
try {
- this.motdUrl.setUrl(new URL(url));
+ this.motdUrl = new URL(url).toString();
} catch (MalformedURLException e) {
- this.motdUrl = new DefaultedURL();
+ this.motdUrl = "";
}
}
public String getApiUrlWithVersion() {
- return getApiUrlString() + "/" + getApiVersion();
+ return getApiUrl() + "/" + getApiVersion();
}
- public String getApiUrlString() {
- return getApiUrl().toString();
+ public String getApiUrl() {
+ return apiUrl;
}
- public String getApiVersion() {
+ public int getApiVersion() {
return apiVersion;
}
@@ -338,7 +492,7 @@ public final class Provider implements Parcelable {
}
public boolean hasDefinition() {
- return definition != null && definition.length() > 0;
+ return (definition != null && definition.length() > 0) || (modelsProvider != null);
}
public boolean hasGeoIpJson() {
@@ -363,7 +517,7 @@ public final class Provider implements Parcelable {
name = definition.getJSONObject(API_TERM_NAME).getString("en");
} catch (JSONException e2) {
if (mainUrl != null) {
- String host = mainUrl.getDomain();
+ String host = getHostFromUrl(mainUrl);
name = host.substring(0, host.indexOf("."));
}
}
@@ -394,12 +548,25 @@ public final class Provider implements Parcelable {
&& !getEipServiceJson().has(ERRORS);
}
+ public boolean hasServiceInfo() {
+ return modelsEIPService != null;
+ }
+
public boolean hasGatewaysInDifferentLocations() {
- try {
- return getEipServiceJson().getJSONObject(LOCATIONS).length() > 1;
- } catch (NullPointerException | JSONException e) {
- return false;
+ if (apiVersion >= 5) {
+ try {
+ return getService().getLocations().size() > 1;
+ } catch (NullPointerException e) {
+ return false;
+ }
+ } else {
+ try {
+ return getEipServiceJson().getJSONObject(LOCATIONS).length() > 1;
+ } catch (NullPointerException | JSONException e) {
+ return false;
+ }
}
+
}
@Override
@@ -410,17 +577,17 @@ public final class Provider implements Parcelable {
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeString(getDomain());
- parcel.writeString(getMainUrlString());
+ parcel.writeString(getMainUrl());
parcel.writeString(getProviderIp());
parcel.writeString(getProviderApiIp());
- parcel.writeString(getGeoipUrl().toString());
- parcel.writeString(getMotdUrl().toString());
+ parcel.writeString(getGeoipUrl());
+ parcel.writeString(getMotdUrl());
parcel.writeString(getDefinitionString());
parcel.writeString(getCaCert());
parcel.writeString(getEipServiceJsonString());
parcel.writeString(getGeoIpJsonString());
parcel.writeString(getMotdJsonString());
- parcel.writeString(getPrivateKey());
+ parcel.writeString(getPrivateKeyString());
parcel.writeString(getVpnCertificate());
parcel.writeLong(lastEipServiceUpdate);
parcel.writeLong(lastGeoIpUpdate);
@@ -428,6 +595,14 @@ public final class Provider implements Parcelable {
parcel.writeLong(lastMotdSeen);
parcel.writeStringList(new ArrayList<>(lastMotdSeenHashes));
parcel.writeInt(shouldUpdateVpnCertificate ? 0 : 1);
+ parcel.writeParcelable(introducer, 0);
+ if (apiVersion == 5) {
+ Gson gson = JSON.createGson().create();
+ parcel.writeString(modelsProvider != null ? gson.toJson(modelsProvider) : "");
+ parcel.writeString(modelsEIPService != null ? gson.toJson(modelsEIPService) : "");
+ parcel.writeString(modelsBridges != null ? gson.toJson(modelsBridges) : "");
+ parcel.writeString(modelsGateways != null ? gson.toJson(modelsGateways) : "");
+ }
}
@@ -435,7 +610,7 @@ public final class Provider implements Parcelable {
private Provider(Parcel in) {
try {
domain = in.readString();
- mainUrl.setUrl(new URL(in.readString()));
+ setMainUrl(in.readString());
String tmpString = in.readString();
if (!tmpString.isEmpty()) {
providerIp = tmpString;
@@ -446,11 +621,11 @@ public final class Provider implements Parcelable {
}
tmpString = in.readString();
if (!tmpString.isEmpty()) {
- geoipUrl.setUrl(new URL(tmpString));
+ geoipUrl = new URL(tmpString).toString();
}
tmpString = in.readString();
if (!tmpString.isEmpty()) {
- motdUrl.setUrl(new URL(tmpString));
+ motdUrl = new URL(tmpString).toString();
}
tmpString = in.readString();
if (!tmpString.isEmpty()) {
@@ -475,7 +650,7 @@ public final class Provider implements Parcelable {
}
tmpString = in.readString();
if (!tmpString.isEmpty()) {
- this.setPrivateKey(tmpString);
+ this.setPrivateKeyString(tmpString);
}
tmpString = in.readString();
if (!tmpString.isEmpty()) {
@@ -489,6 +664,13 @@ public final class Provider implements Parcelable {
in.readStringList(lastMotdSeenHashes);
this.lastMotdSeenHashes = new HashSet<>(lastMotdSeenHashes);
this.shouldUpdateVpnCertificate = in.readInt() == 0;
+ this.introducer = in.readParcelable(Introducer.class.getClassLoader());
+ if (this.apiVersion == 5) {
+ this.setModelsProvider(in.readString());
+ this.setService(in.readString());
+ this.setBridges(in.readString());
+ this.setGateways(in.readString());
+ }
} catch (MalformedURLException | JSONException e) {
e.printStackTrace();
}
@@ -500,24 +682,28 @@ public final class Provider implements Parcelable {
if (o instanceof Provider) {
Provider p = (Provider) o;
return getDomain().equals(p.getDomain()) &&
- mainUrl.getDomain().equals(p.mainUrl.getDomain()) &&
- definition.toString().equals(p.getDefinition().toString()) &&
- eipServiceJson.toString().equals(p.getEipServiceJsonString()) &&
- geoIpJson.toString().equals(p.getGeoIpJsonString()) &&
- motdJson.toString().equals(p.getMotdJsonString()) &&
- providerIp.equals(p.getProviderIp()) &&
- providerApiIp.equals(p.getProviderApiIp()) &&
- apiUrl.equals(p.getApiUrl()) &&
- geoipUrl.equals(p.getGeoipUrl()) &&
- motdUrl.equals(p.getMotdUrl()) &&
- certificatePin.equals(p.getCertificatePin()) &&
- certificatePinEncoding.equals(p.getCertificatePinEncoding()) &&
- caCert.equals(p.getCaCert()) &&
- apiVersion.equals(p.getApiVersion()) &&
- privateKey.equals(p.getPrivateKey()) &&
- vpnCertificate.equals(p.getVpnCertificate()) &&
- allowAnonymous == p.allowsAnonymous() &&
- allowRegistered == p.allowsRegistered();
+ getHostFromUrl(mainUrl).equals(getHostFromUrl(p.getMainUrl())) &&
+ definition.toString().equals(p.getDefinition().toString()) &&
+ eipServiceJson.toString().equals(p.getEipServiceJsonString()) &&
+ geoIpJson.toString().equals(p.getGeoIpJsonString()) &&
+ motdJson.toString().equals(p.getMotdJsonString()) &&
+ providerIp.equals(p.getProviderIp()) &&
+ providerApiIp.equals(p.getProviderApiIp()) &&
+ apiUrl.equals(p.getApiUrl()) &&
+ geoipUrl.equals(p.getGeoipUrl()) &&
+ motdUrl.equals(p.getMotdUrl()) &&
+ certificatePin.equals(p.getCertificatePin()) &&
+ certificatePinEncoding.equals(p.getCertificatePinEncoding()) &&
+ caCert.equals(p.getCaCert()) &&
+ apiVersion == p.getApiVersion() &&
+ privateKeyString.equals(p.getPrivateKeyString()) &&
+ vpnCertificate.equals(p.getVpnCertificate()) &&
+ allowAnonymous == p.allowsAnonymous() &&
+ allowRegistered == p.allowsRegistered() &&
+ Objects.equals(modelsProvider, p.modelsProvider) &&
+ Objects.equals(modelsEIPService, p.modelsEIPService) &&
+ Arrays.equals(modelsBridges, p.modelsBridges) &&
+ Arrays.equals(modelsGateways, p.modelsGateways);
} else return false;
}
@@ -535,7 +721,7 @@ public final class Provider implements Parcelable {
@Override
public int hashCode() {
- return getMainUrlString().hashCode();
+ return getMainUrl().hashCode();
}
@Override
@@ -545,20 +731,65 @@ public final class Provider implements Parcelable {
private boolean parseDefinition(JSONObject definition) {
try {
+ this.apiVersions = parseApiVersionsArray();
+ this.apiVersion = selectPreferredApiVersion();
+ this.domain = getDefinition().getString(Provider.DOMAIN);
String pin = definition.getString(CA_CERT_FINGERPRINT);
this.certificatePin = pin.split(":")[1].trim();
this.certificatePinEncoding = pin.split(":")[0].trim();
- this.apiUrl.setUrl(new URL(definition.getString(API_URL)));
- this.allowAnonymous = definition.getJSONObject(Provider.SERVICE).getBoolean(PROVIDER_ALLOW_ANONYMOUS);
- this.allowRegistered = definition.getJSONObject(Provider.SERVICE).getBoolean(PROVIDER_ALLOWED_REGISTERED);
- this.apiVersion = getDefinition().getString(Provider.API_VERSION);
- this.domain = getDefinition().getString(Provider.DOMAIN);
+ this.apiUrl = new URL(definition.getString(API_URL)).toString();
+ JSONObject serviceJSONObject = definition.getJSONObject(Provider.SERVICE);
+ if (serviceJSONObject.has(PROVIDER_ALLOW_ANONYMOUS)) {
+ this.allowAnonymous = serviceJSONObject.getBoolean(PROVIDER_ALLOW_ANONYMOUS);
+ }
+ if (serviceJSONObject.has(PROVIDER_ALLOWED_REGISTERED)) {
+ this.allowRegistered = serviceJSONObject.getBoolean(PROVIDER_ALLOWED_REGISTERED);
+ }
return true;
- } catch (JSONException | ArrayIndexOutOfBoundsException | MalformedURLException e) {
+ } catch (JSONException | ArrayIndexOutOfBoundsException | MalformedURLException | NullPointerException | NumberFormatException e) {
return false;
}
}
+ /**
+ @returns latest api version supported by client and server or the version set in 'api_version'
+ in case there's not a common supported version
+ */
+ private int selectPreferredApiVersion() throws JSONException, NumberFormatException {
+ if (apiVersions.length == 0) {
+ return Integer.parseInt(getDefinition().getString(Provider.API_VERSION));
+ }
+
+ // apiVersion is a sorted Array
+ for (int i = apiVersions.length -1; i >= 0; i--) {
+ if (apiVersions[i] == BuildConfig.preferred_client_api_version ||
+ apiVersions[i] < BuildConfig.preferred_client_api_version) {
+ return apiVersions[i];
+ }
+ }
+
+ return Integer.parseInt(getDefinition().getString(Provider.API_VERSION));
+ }
+
+ private int[] parseApiVersionsArray() {
+ int[] versionArray = new int[0];
+ try {
+ JSONArray versions = getDefinition().getJSONArray(Provider.API_VERSIONS);
+ versionArray = new int[versions.length()];
+ for (int i = 0; i < versions.length(); i++) {
+ try {
+ versionArray[i] = Integer.parseInt(versions.getString(i));
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ }
+ }
+ } catch (JSONException ignore) {
+ // this backend doesn't support api_versions yet
+ }
+ Arrays.sort(versionArray);
+ return versionArray;
+ }
+
public void setCaCert(String cert) {
this.caCert = cert;
}
@@ -600,7 +831,7 @@ public final class Provider implements Parcelable {
* @return true if last message of the day was shown more than 24h ago
*/
public boolean shouldShowMotdSeen() {
- return !motdUrl.isDefault() && System.currentTimeMillis() - lastMotdSeen >= MOTD_TIMEOUT;
+ return !motdUrl.isEmpty() && System.currentTimeMillis() - lastMotdSeen >= MOTD_TIMEOUT;
}
/**
@@ -636,7 +867,7 @@ public final class Provider implements Parcelable {
}
public boolean shouldUpdateMotdJson() {
- return !motdUrl.isDefault() && System.currentTimeMillis() - lastMotdUpdate >= MOTD_TIMEOUT;
+ return !motdUrl.isEmpty() && System.currentTimeMillis() - lastMotdUpdate >= MOTD_TIMEOUT;
}
public void setMotdJson(@NonNull JSONObject motdJson) {
@@ -693,31 +924,31 @@ public final class Provider implements Parcelable {
}
public boolean isDefault() {
- return getMainUrl().isDefault() &&
- getApiUrl().isDefault() &&
- getGeoipUrl().isDefault() &&
+ return getMainUrl().isEmpty() &&
+ getApiUrl().isEmpty() &&
+ getGeoipUrl().isEmpty() &&
certificatePin.isEmpty() &&
certificatePinEncoding.isEmpty() &&
caCert.isEmpty();
}
- public String getPrivateKey() {
- return privateKey;
+ public String getPrivateKeyString() {
+ return privateKeyString;
}
- public RSAPrivateKey getRSAPrivateKey() {
- if (rsaPrivateKey == null) {
- rsaPrivateKey = parseRsaKeyFromString(privateKey);
+ public PrivateKey getPrivateKey() {
+ if (privateKey == null) {
+ privateKey = parsePrivateKeyFromString(privateKeyString);
}
- return rsaPrivateKey;
+ return privateKey;
}
- public void setPrivateKey(String privateKey) {
- this.privateKey = privateKey;
+ public void setPrivateKeyString(String privateKeyString) {
+ this.privateKeyString = privateKeyString;
}
public boolean hasPrivateKey() {
- return privateKey != null && privateKey.length() > 0;
+ return privateKeyString != null && privateKeyString.length() > 0;
}
public String getVpnCertificate() {
@@ -744,6 +975,18 @@ public final class Provider implements Parcelable {
return getCertificatePinEncoding() + ":" + getCertificatePin();
}
+ public boolean hasIntroducer() {
+ return introducer != null;
+ }
+
+ public Introducer getIntroducer() {
+ return introducer;
+ }
+
+ public void setIntroducer(String introducerUrl) throws URISyntaxException, IllegalArgumentException {
+ this.introducer = Introducer.fromUrl(introducerUrl);
+ }
+
/**
* resets everything except the main url, the providerIp and the geoip
* service url (currently preseeded)
@@ -753,16 +996,21 @@ public final class Provider implements Parcelable {
eipServiceJson = new JSONObject();
geoIpJson = new JSONObject();
motdJson = new JSONObject();
- apiUrl = new DefaultedURL();
+ apiUrl = "";
certificatePin = "";
certificatePinEncoding = "";
caCert = "";
- apiVersion = "";
- privateKey = "";
+ apiVersion = BuildConfig.preferred_client_api_version;
+ privateKeyString = "";
vpnCertificate = "";
allowRegistered = false;
allowAnonymous = false;
lastGeoIpUpdate = 0L;
lastEipServiceUpdate = 0L;
+ modelsProvider = null;
+ modelsGateways = null;
+ modelsBridges = null;
+ modelsEIPService = null;
}
+
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java
index 33fbbf7a..d0149a3f 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java
@@ -1,5 +1,21 @@
package se.leap.bitmaskclient.base.models;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_HOP;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN;
+import static se.leap.bitmaskclient.base.models.Constants.CAPABILITIES;
+import static se.leap.bitmaskclient.base.models.Constants.CERT;
+import static se.leap.bitmaskclient.base.models.Constants.HOP_JITTER;
+import static se.leap.bitmaskclient.base.models.Constants.IAT_MODE;
+import static se.leap.bitmaskclient.base.models.Constants.MAX_HOP_PORT;
+import static se.leap.bitmaskclient.base.models.Constants.MIN_HOP_PORT;
+import static se.leap.bitmaskclient.base.models.Constants.MIN_HOP_SECONDS;
+import static se.leap.bitmaskclient.base.models.Constants.PORTS;
+import static se.leap.bitmaskclient.base.models.Constants.PORT_COUNT;
+import static se.leap.bitmaskclient.base.models.Constants.PORT_SEED;
+import static se.leap.bitmaskclient.base.models.Constants.PROTOCOLS;
+import static se.leap.bitmaskclient.base.models.Constants.TRANSPORT;
+
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.FieldNamingPolicy;
@@ -7,31 +23,44 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.SerializedName;
+import org.json.JSONArray;
import org.json.JSONObject;
import java.io.Serializable;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Vector;
import de.blinkt.openvpn.core.connection.Connection;
+import io.swagger.client.model.ModelsBridge;
+import io.swagger.client.model.ModelsGateway;
public class Transport implements Serializable {
- private String type;
- private String[] protocols;
+ private final String type;
+ private final String[] protocols;
@Nullable
- private String[] ports;
+ private final String[] ports;
@Nullable
- private Options options;
+ private final Options options;
public Transport(String type, String[] protocols, String[] ports, String cert) {
this(type, protocols, ports, new Options(cert, "0"));
}
- public Transport(String type, String[] protocols, String[] ports, Options options) {
+ public Transport(String type, String[] protocols, @Nullable String[] ports, @Nullable Options options) {
this.type = type;
this.protocols = protocols;
this.ports = ports;
this.options = options;
}
+ public Transport(String type, String[] protocols, @Nullable String[] ports) {
+ this.type = type;
+ this.protocols = protocols;
+ this.ports = ports;
+ this.options = null;
+ }
+
public String getType() {
return type;
}
@@ -67,12 +96,107 @@ public class Transport implements Serializable {
fromJson(json.toString(), Transport.class);
}
+ public static Transport createTransportFrom(ModelsBridge modelsBridge) {
+ if (modelsBridge == null) {
+ return null;
+ }
+ Map<String, Object> options = modelsBridge.getOptions();
+ Transport.Options transportOptions = new Transport.Options((String) options.get(CERT), (String) options.get(IAT_MODE));
+ if (OBFS4_HOP.toString().equals(modelsBridge.getType())) {
+ transportOptions.minHopSeconds = getIntOption(options, MIN_HOP_SECONDS, 5);
+ transportOptions.minHopPort = getIntOption(options, MIN_HOP_PORT, 49152);
+ transportOptions.maxHopPort = getIntOption(options, MAX_HOP_PORT, 65535);
+ transportOptions.hopJitter = getIntOption(options, HOP_JITTER, 10);
+ transportOptions.portCount = getIntOption(options, PORT_COUNT, 100);
+ transportOptions.portSeed = getIntOption(options, PORT_SEED, 1);
+ }
+ Transport transport = new Transport(
+ modelsBridge.getType(),
+ new String[]{modelsBridge.getTransport()},
+ new String[]{String.valueOf(modelsBridge.getPort())},
+ transportOptions
+ );
+ return transport;
+ }
+
+ private static int getIntOption(Map<String, Object> options, String key, int defaultValue) {
+ try {
+ Object o = options.get(key);
+ if (o == null) {
+ return defaultValue;
+ }
+ if (o instanceof String) {
+ return Integer.parseInt((String) o);
+ }
+ return (int) o;
+ } catch (NullPointerException | ClassCastException | NumberFormatException e){
+ e.printStackTrace();
+ return defaultValue;
+ }
+ }
+
+ public static Transport createTransportFrom(ModelsGateway modelsGateway) {
+ if (modelsGateway == null) {
+ return null;
+ }
+ Transport transport = new Transport(
+ modelsGateway.getType(),
+ new String[]{modelsGateway.getTransport()},
+ new String[]{String.valueOf(modelsGateway.getPort())}
+ );
+ return transport;
+ }
+
+
+ @NonNull
+ public static Vector<Transport> createTransportsFrom(JSONObject gateway, int apiVersion) throws IllegalArgumentException {
+ Vector<Transport> transports = new Vector<>();
+ try {
+ if (apiVersion >= 3) {
+ JSONArray supportedTransports = gateway.getJSONObject(CAPABILITIES).getJSONArray(TRANSPORT);
+ for (int i = 0; i < supportedTransports.length(); i++) {
+ Transport transport = Transport.fromJson(supportedTransports.getJSONObject(i));
+ transports.add(transport);
+ }
+ } else {
+ JSONObject capabilities = gateway.getJSONObject(CAPABILITIES);
+ JSONArray ports = capabilities.getJSONArray(PORTS);
+ JSONArray protocols = capabilities.getJSONArray(PROTOCOLS);
+ String[] portArray = new String[ports.length()];
+ String[] protocolArray = new String[protocols.length()];
+ for (int i = 0; i < ports.length(); i++) {
+ portArray[i] = String.valueOf(ports.get(i));
+ }
+ for (int i = 0; i < protocols.length(); i++) {
+ protocolArray[i] = protocols.optString(i);
+ }
+ Transport transport = new Transport(OPENVPN.toString(), protocolArray, portArray);
+ transports.add(transport);
+ }
+ } catch (Exception e) {
+ throw new IllegalArgumentException();
+ //throw new ConfigParser.ConfigParseError("Api version ("+ apiVersion +") did not match required JSON fields");
+ }
+ return transports;
+ }
+
+ public static Vector<Transport> createTransportsFrom(ModelsBridge modelsBridge) {
+ Vector<Transport> transports = new Vector<>();
+ transports.add(Transport.createTransportFrom(modelsBridge));
+ return transports;
+ }
+
+ public static Vector<Transport> createTransportsFrom(ModelsGateway modelsGateway) {
+ Vector<Transport> transports = new Vector<>();
+ transports.add(Transport.createTransportFrom(modelsGateway));
+ return transports;
+ }
+
public static class Options implements Serializable {
@Nullable
- private String cert;
+ private final String cert;
@SerializedName("iatMode")
- private String iatMode;
-
+ private final String iatMode;
@Nullable
private Endpoint[] endpoints;
@@ -81,23 +205,30 @@ public class Transport implements Serializable {
private int portSeed;
private int portCount;
-
+ private int minHopPort;
+ private int maxHopPort;
+ private int minHopSeconds;
+ private int hopJitter;
public Options(String cert, String iatMode) {
this.cert = cert;
this.iatMode = iatMode;
}
- public Options(String iatMode, Endpoint[] endpoints, int portSeed, int portCount, boolean experimental) {
- this(iatMode, endpoints, null, portSeed, portCount, experimental);
+ public Options(String iatMode, Endpoint[] endpoints, int portSeed, int portCount, int minHopPort, int maxHopPort, int minHopSeconds, int hopJitter, boolean experimental) {
+ this(iatMode, endpoints, null, portSeed, portCount, minHopPort, maxHopPort, minHopSeconds, hopJitter, experimental);
}
- public Options(String iatMode, Endpoint[] endpoints, String cert, int portSeed, int portCount, boolean experimental) {
+ public Options(String iatMode, Endpoint[] endpoints, String cert, int portSeed, int portCount, int minHopPort, int maxHopPort, int minHopSeconds, int hopJitter, boolean experimental) {
this.iatMode = iatMode;
this.endpoints = endpoints;
this.portSeed = portSeed;
this.portCount = portCount;
this.experimental = experimental;
+ this.minHopPort = minHopPort;
+ this.maxHopPort = maxHopPort;
+ this.minHopSeconds = minHopSeconds;
+ this.hopJitter = hopJitter;
this.cert = cert;
}
@@ -128,6 +259,22 @@ public class Transport implements Serializable {
return portCount;
}
+ public int getMinHopPort() {
+ return minHopPort;
+ }
+
+ public int getMaxHopPort() {
+ return maxHopPort;
+ }
+
+ public int getMinHopSeconds() {
+ return minHopSeconds;
+ }
+
+ public int getHopJitter() {
+ return hopJitter;
+ }
+
@Override
public String toString() {
return new Gson().toJson(this);
@@ -136,8 +283,8 @@ public class Transport implements Serializable {
public static class Endpoint implements Serializable {
- private String ip;
- private String cert;
+ private final String ip;
+ private final String cert;
public Endpoint(String ip, String cert) {
this.ip = ip;
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/BitmaskCoreProvider.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/BitmaskCoreProvider.java
new file mode 100644
index 00000000..77cf9cf0
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/BitmaskCoreProvider.java
@@ -0,0 +1,28 @@
+package se.leap.bitmaskclient.base.utils;
+
+import de.blinkt.openvpn.core.NativeUtils;
+import mobile.BitmaskMobile;
+import mobilemodels.BitmaskMobileCore;
+
+public class BitmaskCoreProvider {
+ private static BitmaskMobileCore customMobileCore;
+
+ /**
+ * Returns an empty BitmaskMobile instance, which can be currently only used to access
+ * bitmask-core's persistence layer API
+ * @return BitmaskMobileCore interface
+ */
+ public static BitmaskMobileCore getBitmaskMobile() {
+ if (customMobileCore == null) {
+ return new BitmaskMobile(new PreferenceHelper.SharedPreferenceStore());
+ }
+ return customMobileCore;
+ }
+
+ public static void initBitmaskMobile(BitmaskMobileCore bitmaskMobileCore) {
+ if (!NativeUtils.isUnitTest()) {
+ throw new IllegalStateException("Initializing custom BitmaskMobileCore implementation outside of an unit test is not allowed");
+ }
+ BitmaskCoreProvider.customMobileCore = bitmaskMobileCore;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/BuildConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/BuildConfigHelper.java
index e1f65b5e..22939611 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/utils/BuildConfigHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/BuildConfigHelper.java
@@ -11,29 +11,26 @@ import se.leap.bitmaskclient.BuildConfig;
public class BuildConfigHelper {
public interface BuildConfigHelperInterface {
- boolean useObfsVpn();
boolean hasObfuscationPinningDefaults();
String obfsvpnIP();
String obfsvpnPort();
String obfsvpnCert();
- boolean useKcp();
+ String obfsvpnTransportProtocol();
boolean isDefaultBitmask();
}
public static class DefaultBuildConfigHelper implements BuildConfigHelperInterface {
- @Override
- public boolean useObfsVpn() {
- return BuildConfig.use_obfsvpn;
- }
@Override
public boolean hasObfuscationPinningDefaults() {
return BuildConfig.obfsvpn_ip != null &&
BuildConfig.obfsvpn_port != null &&
BuildConfig.obfsvpn_cert != null &&
+ BuildConfig.obfsvpn_transport_protocol != null &&
!BuildConfig.obfsvpn_ip.isEmpty() &&
!BuildConfig.obfsvpn_port.isEmpty() &&
- !BuildConfig.obfsvpn_cert.isEmpty();
+ !BuildConfig.obfsvpn_cert.isEmpty() &&
+ !BuildConfig.obfsvpn_transport_protocol.isEmpty();
}
@Override
@@ -52,8 +49,8 @@ public class BuildConfigHelper {
}
@Override
- public boolean useKcp() {
- return BuildConfig.obfsvpn_use_kcp;
+ public String obfsvpnTransportProtocol() {
+ return BuildConfig.obfsvpn_transport_protocol;
}
@Override
@@ -72,10 +69,6 @@ public class BuildConfigHelper {
instance = helperInterface;
}
- public static boolean useObfsVpn() {
- return instance.useObfsVpn();
- }
-
public static boolean hasObfuscationPinningDefaults() {
return instance.hasObfuscationPinningDefaults();
}
@@ -88,8 +81,8 @@ public class BuildConfigHelper {
public static String obfsvpnCert() {
return instance.obfsvpnCert();
}
- public static boolean useKcp() {
- return instance.useKcp();
+ public static String obfsvpnTransportProtocol() {
+ return instance.obfsvpnTransportProtocol();
}
public static boolean isDefaultBitmask() {
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java
index cd5d1fca..74328f45 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java
@@ -21,6 +21,8 @@ import android.content.Context;
import android.content.res.Resources;
import android.os.Build;
import android.os.Looper;
+import android.util.Patterns;
+import android.webkit.URLUtil;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -34,6 +36,7 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
+import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
@@ -119,6 +122,36 @@ public class ConfigHelper {
return null;
}
+ public static String parseX509CertificatesToString(ArrayList<X509Certificate> certs) {
+ StringBuilder sb = new StringBuilder();
+ for (X509Certificate certificate : certs) {
+
+ byte[] derCert = new byte[0];
+ try {
+ derCert = certificate.getEncoded();
+ byte[] encodedCert = Base64.encode(derCert);
+ String base64Cert = new String(encodedCert);
+
+ // add cert header
+ sb.append("-----BEGIN CERTIFICATE-----\n");
+
+ // split base64 string into lines of 64 characters
+ int index = 0;
+ while (index < base64Cert.length()) {
+ sb.append(base64Cert.substring(index, Math.min(index + 64, base64Cert.length())))
+ .append("\n");
+ index += 64;
+ }
+
+ // add cert footer
+ sb.append("-----END CERTIFICATE-----\n");
+ } catch (CertificateEncodingException e) {
+ e.printStackTrace();
+ }
+ }
+ return sb.toString().trim();
+ }
+
public static void ensureNotOnMainThread(@NonNull Context context) throws IllegalStateException{
Looper looper = Looper.myLooper();
if (looper != null && looper == context.getMainLooper()) {
@@ -190,8 +223,28 @@ public class ConfigHelper {
return matcher.matches();
}
- public static String getDomainFromMainURL(@NonNull String mainUrl) throws NullPointerException {
- return PublicSuffixDatabase.Companion.get().getEffectiveTldPlusOne(mainUrl).replaceFirst("http[s]?://", "").replaceFirst("/.*", "");
+ public static boolean isNetworkUrl(String url) {
+ return url != null && URLUtil.isNetworkUrl(url)
+ && URLUtil.isHttpsUrl(url)
+ && Patterns.WEB_URL.matcher(url).matches();
+ }
+
+ public static boolean isDomainName(String url) {
+ return url != null && Patterns.DOMAIN_NAME.matcher(url).matches();
+ }
+
+ /**
+ * Extracts a domain from a given URL
+ * @param mainUrl URL as String
+ * @return Domain as String, null if mainUrl is an invalid URL
+ */
+ public static String getDomainFromMainURL(String mainUrl) {
+ try {
+ String topLevelDomain = PublicSuffixDatabase.Companion.get().getEffectiveTldPlusOne(mainUrl);
+ return topLevelDomain.replaceFirst("http[s]?://", "").replaceFirst("/.*", "");
+ } catch (NullPointerException | IllegalArgumentException e) {
+ return null;
+ }
}
public static boolean isCalyxOSWithTetheringSupport(Context context) {
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/CredentialsParser.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/CredentialsParser.java
new file mode 100644
index 00000000..a62d548a
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/CredentialsParser.java
@@ -0,0 +1,58 @@
+package se.leap.bitmaskclient.base.utils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+import se.leap.bitmaskclient.base.models.Provider;
+
+public class CredentialsParser {
+
+ public static void parseXml(String xmlString, Provider provider) throws XmlPullParserException, IOException {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlPullParser parser = factory.newPullParser();
+ parser.setInput(new StringReader(xmlString));
+
+ String currentTag = null;
+ String ca = null;
+ String key = null;
+ String cert = null;
+
+ int eventType = parser.getEventType();
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ switch (eventType) {
+ case XmlPullParser.START_TAG -> currentTag = parser.getName();
+ case XmlPullParser.TEXT -> {
+ if (currentTag != null) {
+ switch (currentTag) {
+ case "ca" -> {
+ ca = parser.getText();
+ ca = ca.trim();
+ }
+ case "key" -> {
+ key = parser.getText();
+ key = key.trim();
+ }
+ case "cert" -> {
+ cert = parser.getText();
+ cert = cert.trim();
+ }
+ }
+ }
+ }
+ case XmlPullParser.END_TAG -> currentTag = null;
+ }
+ eventType = parser.next();
+ }
+
+ provider.setCaCert(ca);
+ provider.setPrivateKeyString(key);
+ provider.setVpnCertificate(cert);
+
+ }
+}
+
+
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java
index f1d86876..9d29911b 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java
@@ -70,7 +70,7 @@ public class FileHelper {
}
reader.close();
return sb.toString();
- } catch (IOException errabi) {
+ } catch (NullPointerException | IOException errabi) {
return null;
}
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
index eee3bfb2..ba644b91 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
@@ -1,12 +1,18 @@
package se.leap.bitmaskclient.base.utils;
import static android.content.Context.MODE_PRIVATE;
+import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.TCP;
+import static se.leap.bitmaskclient.base.fragments.CensorshipCircumventionFragment.TUNNELING_AUTOMATICALLY;
+import static se.leap.bitmaskclient.base.fragments.CensorshipCircumventionFragment.TUNNELING_OBFS4;
+import static se.leap.bitmaskclient.base.fragments.CensorshipCircumventionFragment.TUNNELING_OBFS4_KCP;
+import static se.leap.bitmaskclient.base.fragments.CensorshipCircumventionFragment.TUNNELING_QUIC;
import static se.leap.bitmaskclient.base.models.Constants.ALLOW_EXPERIMENTAL_TRANSPORTS;
import static se.leap.bitmaskclient.base.models.Constants.ALLOW_TETHERING_BLUETOOTH;
import static se.leap.bitmaskclient.base.models.Constants.ALLOW_TETHERING_USB;
import static se.leap.bitmaskclient.base.models.Constants.ALLOW_TETHERING_WIFI;
import static se.leap.bitmaskclient.base.models.Constants.ALWAYS_ON_SHOW_DIALOG;
import static se.leap.bitmaskclient.base.models.Constants.CLEARLOG;
+import static se.leap.bitmaskclient.base.models.Constants.COUNTRYCODE;
import static se.leap.bitmaskclient.base.models.Constants.CUSTOM_PROVIDER_DOMAINS;
import static se.leap.bitmaskclient.base.models.Constants.DEFAULT_SHARED_PREFS_BATTERY_SAVER;
import static se.leap.bitmaskclient.base.models.Constants.EIP_IS_ALWAYS_ON;
@@ -19,14 +25,18 @@ import static se.leap.bitmaskclient.base.models.Constants.LAST_UPDATE_CHECK;
import static se.leap.bitmaskclient.base.models.Constants.LAST_USED_PROFILE;
import static se.leap.bitmaskclient.base.models.Constants.OBFUSCATION_PINNING_CERT;
import static se.leap.bitmaskclient.base.models.Constants.OBFUSCATION_PINNING_IP;
-import static se.leap.bitmaskclient.base.models.Constants.OBFUSCATION_PINNING_KCP;
import static se.leap.bitmaskclient.base.models.Constants.OBFUSCATION_PINNING_LOCATION;
import static se.leap.bitmaskclient.base.models.Constants.OBFUSCATION_PINNING_PORT;
+import static se.leap.bitmaskclient.base.models.Constants.OBFUSCATION_PINNING_PROTOCOL;
import static se.leap.bitmaskclient.base.models.Constants.PREFERENCES_APP_VERSION;
import static se.leap.bitmaskclient.base.models.Constants.PREFERRED_CITY;
import static se.leap.bitmaskclient.base.models.Constants.PREFER_UDP;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_CONFIGURED;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_EIP_DEFINITION;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MODELS_BRIDGES;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MODELS_EIPSERVICE;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MODELS_GATEWAYS;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MODELS_PROVIDER;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_HASHES;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_LAST_SEEN;
@@ -40,12 +50,16 @@ import static se.leap.bitmaskclient.base.models.Constants.SHOW_EXPERIMENTAL;
import static se.leap.bitmaskclient.base.models.Constants.USE_BRIDGES;
import static se.leap.bitmaskclient.base.models.Constants.USE_IPv6_FIREWALL;
import static se.leap.bitmaskclient.base.models.Constants.USE_OBFUSCATION_PINNING;
+import static se.leap.bitmaskclient.base.models.Constants.USE_PORT_HOPPING;
import static se.leap.bitmaskclient.base.models.Constants.USE_SNOWFLAKE;
import static se.leap.bitmaskclient.base.models.Constants.USE_SYSTEM_PROXY;
+import static se.leap.bitmaskclient.base.models.Constants.USE_TUNNEL;
+import static se.leap.bitmaskclient.base.utils.BitmaskCoreProvider.getBitmaskMobile;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
+import android.util.Base64;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
@@ -57,9 +71,9 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
-import java.net.MalformedURLException;
import java.net.URL;
import java.security.GeneralSecurityException;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@@ -68,6 +82,7 @@ import java.util.Set;
import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.NativeUtils;
import se.leap.bitmaskclient.BuildConfig;
+import se.leap.bitmaskclient.base.models.Introducer;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.tor.TorStatusObservable;
@@ -143,13 +158,19 @@ public class PreferenceHelper {
provider.define(new JSONObject(preferences.getString(Provider.KEY, "")));
provider.setCaCert(preferences.getString(Provider.CA_CERT, ""));
provider.setVpnCertificate(preferences.getString(PROVIDER_VPN_CERTIFICATE, ""));
- provider.setPrivateKey(preferences.getString(PROVIDER_PRIVATE_KEY, ""));
+ provider.setPrivateKeyString(preferences.getString(PROVIDER_PRIVATE_KEY, ""));
provider.setEipServiceJson(new JSONObject(preferences.getString(PROVIDER_EIP_DEFINITION, "")));
provider.setMotdJson(new JSONObject(preferences.getString(PROVIDER_MOTD, "")));
provider.setLastMotdSeen(preferences.getLong(PROVIDER_MOTD_LAST_SEEN, 0L));
provider.setLastMotdUpdate(preferences.getLong(PROVIDER_MOTD_LAST_UPDATED, 0L));
provider.setMotdLastSeenHashes(preferences.getStringSet(PROVIDER_MOTD_HASHES, new HashSet<>()));
- } catch (MalformedURLException | JSONException e) {
+ provider.setModelsProvider(preferences.getString(PROVIDER_MODELS_PROVIDER, null));
+ provider.setService(preferences.getString(PROVIDER_MODELS_EIPSERVICE, null));
+ provider.setBridges(preferences.getString(PROVIDER_MODELS_BRIDGES, null));
+ provider.setGateways(preferences.getString(PROVIDER_MODELS_GATEWAYS, null));
+ provider.setIntroducer(getBitmaskMobile().getIntroducerURLByDomain(provider.getDomain()));
+
+ } catch (Exception e) {
e.printStackTrace();
}
}
@@ -197,9 +218,16 @@ public class PreferenceHelper {
for (String domain : providerDomains) {
String mainURL = preferences.getString(Provider.MAIN_URL + "." + domain, null);
if (mainURL != null) {
- customProviders.put(mainURL, Provider.createCustomProvider(mainURL, domain));
+ Introducer introducer = null;
+ try {
+ introducer = Introducer.fromUrl(BitmaskCoreProvider.getBitmaskMobile().getIntroducerURLByDomain(domain));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ customProviders.put(mainURL, Provider.createCustomProvider(mainURL, domain, introducer));
}
}
+
return customProviders;
}
@@ -210,7 +238,7 @@ public class PreferenceHelper {
SharedPreferences.Editor editor = preferences.edit();
for (Provider provider : providers) {
String providerDomain = provider.getDomain();
- editor.putString(Provider.MAIN_URL + "." + providerDomain, provider.getMainUrlString());
+ editor.putString(Provider.MAIN_URL + "." + providerDomain, provider.getMainUrl());
newProviderDomains.add(providerDomain);
}
@@ -238,16 +266,20 @@ public class PreferenceHelper {
putString(Provider.GEOIP_URL, provider.getGeoipUrl().toString()).
putString(Provider.MOTD_URL, provider.getMotdUrl().toString()).
putString(Provider.PROVIDER_API_IP, provider.getProviderApiIp()).
- putString(Provider.MAIN_URL, provider.getMainUrlString()).
+ putString(Provider.MAIN_URL, provider.getMainUrl()).
putString(Provider.KEY, provider.getDefinitionString()).
putString(Provider.CA_CERT, provider.getCaCert()).
putString(PROVIDER_EIP_DEFINITION, provider.getEipServiceJsonString()).
- putString(PROVIDER_PRIVATE_KEY, provider.getPrivateKey()).
+ putString(PROVIDER_PRIVATE_KEY, provider.getPrivateKeyString()).
putString(PROVIDER_VPN_CERTIFICATE, provider.getVpnCertificate()).
putString(PROVIDER_MOTD, provider.getMotdJsonString()).
putStringSet(PROVIDER_MOTD_HASHES, provider.getMotdLastSeenHashes()).
putLong(PROVIDER_MOTD_LAST_SEEN, provider.getLastMotdSeen()).
- putLong(PROVIDER_MOTD_LAST_UPDATED, provider.getLastMotdUpdate());
+ putLong(PROVIDER_MOTD_LAST_UPDATED, provider.getLastMotdUpdate()).
+ putString(PROVIDER_MODELS_GATEWAYS, provider.getGatewaysJson()).
+ putString(PROVIDER_MODELS_BRIDGES, provider.getBridgesJson()).
+ putString(PROVIDER_MODELS_EIPSERVICE, provider.getServiceJson()).
+ putString(PROVIDER_MODELS_PROVIDER, provider.getModelsProviderJson());
if (async) {
editor.apply();
} else {
@@ -258,9 +290,9 @@ public class PreferenceHelper {
preferences.edit().putBoolean(PROVIDER_CONFIGURED, true).
putString(Provider.PROVIDER_IP + "." + providerDomain, provider.getProviderIp()).
putString(Provider.PROVIDER_API_IP + "." + providerDomain, provider.getProviderApiIp()).
- putString(Provider.MAIN_URL + "." + providerDomain, provider.getMainUrlString()).
- putString(Provider.GEOIP_URL + "." + providerDomain, provider.getGeoipUrl().toString()).
- putString(Provider.MOTD_URL + "." + providerDomain, provider.getMotdUrl().toString()).
+ putString(Provider.MAIN_URL + "." + providerDomain, provider.getMainUrl()).
+ putString(Provider.GEOIP_URL + "." + providerDomain, provider.getGeoipUrl()).
+ putString(Provider.MOTD_URL + "." + providerDomain, provider.getMotdUrl()).
putString(Provider.KEY + "." + providerDomain, provider.getDefinitionString()).
putString(Provider.CA_CERT + "." + providerDomain, provider.getCaCert()).
putString(PROVIDER_EIP_DEFINITION + "." + providerDomain, provider.getEipServiceJsonString()).
@@ -445,6 +477,7 @@ public class PreferenceHelper {
putBoolean(USE_SNOWFLAKE, isEnabled);
if (!isEnabled) {
TorStatusObservable.setProxyPort(-1);
+ TorStatusObservable.setSocksProxyPort(-1);
}
}
@@ -452,6 +485,10 @@ public class PreferenceHelper {
return hasKey(USE_SNOWFLAKE);
}
+ public static void resetSnowflakeSettings() {
+ removeKey(USE_SNOWFLAKE);
+ }
+
public static Boolean getUseSnowflake() {
return getBoolean(USE_SNOWFLAKE, true);
}
@@ -513,8 +550,7 @@ public class PreferenceHelper {
}
public static boolean useObfuscationPinning() {
- return BuildConfigHelper.useObfsVpn() &&
- getUseBridges() &&
+ return getUseBridges() &&
getBoolean(USE_OBFUSCATION_PINNING, false) &&
!TextUtils.isEmpty(getObfuscationPinningIP()) &&
!TextUtils.isEmpty(getObfuscationPinningCert()) &&
@@ -553,18 +589,54 @@ public class PreferenceHelper {
return getString(OBFUSCATION_PINNING_LOCATION, null);
}
- public static Boolean getObfuscationPinningKCP() {
- return getBoolean(OBFUSCATION_PINNING_KCP, false);
+ public static String getObfuscationPinningProtocol() {
+ return getString(OBFUSCATION_PINNING_PROTOCOL, TCP.toString());
}
- public static void setObfuscationPinningKCP(boolean isKCP) {
- putBoolean(OBFUSCATION_PINNING_KCP, isKCP);
+ public static void setObfuscationPinningProtocol(String protocol) {
+ putString(OBFUSCATION_PINNING_PROTOCOL, protocol);
}
public static void setUseIPv6Firewall(boolean useFirewall) {
putBoolean(USE_IPv6_FIREWALL, useFirewall);
}
+ public static boolean getUsePortHopping() {
+ return getBoolean(USE_PORT_HOPPING, false);
+ }
+
+ public static void setUsePortHopping(boolean usePortHopping) {
+ putBoolean(USE_PORT_HOPPING, usePortHopping);
+ }
+
+ public static boolean getUseObfs4() {
+ return getUseTunnel() == TUNNELING_OBFS4;
+ }
+
+ public static boolean getUseObfs4Kcp() {
+ return getUseTunnel() == TUNNELING_OBFS4_KCP;
+ }
+
+ public static boolean getUseObfs4Quic() {
+ return getUseTunnel() == TUNNELING_QUIC;
+ }
+
+ public static boolean useManualBridgeSettings() {
+ return getUseObfs4() || getUseObfs4Kcp() || getUseObfs4Quic() || getUsePortHopping();
+ }
+
+ public static boolean useManualDiscoverySettings() {
+ return hasSnowflakePrefs() && getUseSnowflake();
+ }
+
+ public static void setUseTunnel(int tunnel) {
+ putInt(USE_TUNNEL, tunnel);
+ }
+
+ public static int getUseTunnel() {
+ return getInt(USE_TUNNEL, TUNNELING_AUTOMATICALLY);
+ }
+
public static boolean useIpv6Firewall() {
return getBoolean(USE_IPv6_FIREWALL, false);
}
@@ -577,6 +649,14 @@ public class PreferenceHelper {
return getBoolean(ALWAYS_ON_SHOW_DIALOG, true);
}
+ public static String getBaseCountry() {
+ return getString(COUNTRYCODE, null);
+ }
+
+ public static void setBaseCountry(String countryCode) {
+ putString(COUNTRYCODE, countryCode);
+ }
+
public static String getPreferredCity() {
return useObfuscationPinning() ? null : getString(PREFERRED_CITY, null);
}
@@ -631,6 +711,12 @@ public class PreferenceHelper {
}
}
+ public static void removeKey(String key) {
+ synchronized (LOCK) {
+ preferences.edit().remove(key).apply();
+ }
+ }
+
public static long getLong(String key, long defValue) {
synchronized (LOCK) {
return preferences.getLong(key, defValue);
@@ -738,4 +824,124 @@ public class PreferenceHelper {
preferences.edit().clear().apply();
}
}
+
+ public static class SharedPreferenceStore implements mobilemodels.Store {
+
+ public SharedPreferenceStore() throws IllegalArgumentException {
+ if (preferences == null) {
+ throw new IllegalStateException("Preferences not initialized.");
+ }
+ }
+ @Override
+ public void clear() throws Exception {
+ preferences.edit().clear().apply();
+ }
+
+ @Override
+ public void close() throws Exception {
+
+ }
+
+ @Override
+ public boolean contains(String s) throws Exception {
+ return preferences.contains(s);
+ }
+
+ @Override
+ public boolean getBoolean(String s) {
+ return preferences.getBoolean(s, false);
+ }
+
+ @Override
+ public boolean getBooleanWithDefault(String s, boolean b) {
+ return preferences.getBoolean(s, b);
+ }
+
+ @Override
+ public byte[] getByteArray(String s) {
+ String encodedString = preferences.getString(s, Arrays.toString(Base64.encode(new byte[0], Base64.DEFAULT)));
+ try {
+ return Base64.decode(encodedString, Base64.DEFAULT);
+ } catch (IllegalArgumentException e) {
+ return new byte[0];
+ }
+ }
+
+ @Override
+ public byte[] getByteArrayWithDefault(String s, byte[] bytes) {
+ String encodedString = preferences.getString(s, "");
+ try {
+ return Base64.decode(encodedString, Base64.DEFAULT);
+ } catch (IllegalArgumentException e) {
+ return bytes;
+ }
+ }
+
+ @Override
+ public long getInt(String s) {
+ return preferences.getInt(s, 0);
+ }
+
+ @Override
+ public long getIntWithDefault(String s, long l) {
+ return preferences.getInt(s, (int) l);
+ }
+
+ @Override
+ public long getLong(String s) {
+ return preferences.getLong(s, 0L);
+ }
+
+ @Override
+ public long getLongWithDefault(String s, long l) {
+ return preferences.getLong(s, l);
+ }
+
+ @Override
+ public String getString(String s) {
+ return preferences.getString(s, "");
+ }
+
+ @Override
+ public String getStringWithDefault(String s, String s1) {
+ return preferences.getString(s, s1);
+ }
+
+ @Override
+ public void open() throws Exception {
+
+ }
+
+ @Override
+ public void remove(String s) throws Exception {
+ preferences.edit().remove(s).apply();
+ }
+
+ @Override
+ public void setBoolean(String s, boolean b) {
+ preferences.edit().putBoolean(s, b).apply();
+ }
+
+ @Override
+ public void setByteArray(String s, byte[] bytes) {
+ String encodedString = Base64.encodeToString(bytes, Base64.DEFAULT);
+ preferences.edit().putString(s, encodedString).apply();
+ }
+
+ @Override
+ public void setInt(String s, long l) {
+ preferences.edit().putInt(s, (int) l).apply();
+ }
+
+ @Override
+ public void setLong(String s, long l) {
+ preferences.edit().putLong(s, l).apply();
+ }
+
+ @Override
+ public void setString(String s, String s1) {
+ preferences.edit().putString(s, s1).apply();
+ }
+ }
+
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/PrivateKeyHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/PrivateKeyHelper.java
new file mode 100644
index 00000000..43af5200
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/PrivateKeyHelper.java
@@ -0,0 +1,124 @@
+package se.leap.bitmaskclient.base.utils;
+
+import static android.util.Base64.encodeToString;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import org.spongycastle.util.encoders.Base64;
+
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PrivateKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+
+import de.blinkt.openvpn.core.NativeUtils;
+
+public class PrivateKeyHelper {
+
+ public static final String TAG = PrivateKeyHelper.class.getSimpleName();
+
+ public static final String RSA = "RSA";
+ public static final String ED_25519 = "Ed25519";
+ public static final String ECDSA = "ECDSA";
+
+ public static final String RSA_KEY_BEGIN = "-----BEGIN RSA PRIVATE KEY-----\n";
+ public static final String RSA_KEY_END = "-----END RSA PRIVATE KEY-----";
+ public static final String EC_KEY_BEGIN = "-----BEGIN PRIVATE KEY-----\n";
+ public static final String EC_KEY_END = "-----END PRIVATE KEY-----";
+
+
+ public interface PrivateKeyHelperInterface {
+
+
+ @Nullable PrivateKey parsePrivateKeyFromString(String privateKeyString);
+ }
+
+ public static class DefaultPrivateKeyHelper implements PrivateKeyHelperInterface {
+
+ public PrivateKey parsePrivateKeyFromString(String privateKeyString) {
+ if (privateKeyString == null || privateKeyString.isBlank()) {
+ return null;
+ }
+ if (privateKeyString.contains(RSA_KEY_BEGIN)) {
+ return parseRsaKeyFromString(privateKeyString);
+ } else if (privateKeyString.contains(EC_KEY_BEGIN)) {
+ return parseECPrivateKey(privateKeyString);
+ } else {
+ return null;
+ }
+ }
+
+ private RSAPrivateKey parseRsaKeyFromString(String rsaKeyString) {
+ RSAPrivateKey key;
+ try {
+ KeyFactory kf;
+ kf = KeyFactory.getInstance(RSA, "BC");
+ rsaKeyString = rsaKeyString.replaceFirst(RSA_KEY_BEGIN, "").replaceFirst(RSA_KEY_END, "");
+
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(rsaKeyString));
+ key = (RSAPrivateKey) kf.generatePrivate(keySpec);
+ } catch (InvalidKeySpecException | NoSuchAlgorithmException | NullPointerException |
+ NoSuchProviderException e) {
+ e.printStackTrace();
+ return null;
+ }
+
+ return key;
+ }
+
+ private PrivateKey parseECPrivateKey(String ecKeyString) {
+ String base64 = ecKeyString.replace(EC_KEY_BEGIN, "").replace(EC_KEY_END, "");
+ byte[] keyBytes = Base64.decode(base64);
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
+ String errMsg;
+ try {
+ KeyFactory keyFactory = KeyFactory.getInstance(ED_25519, "BC");
+ return keyFactory.generatePrivate(keySpec);
+ } catch (InvalidKeySpecException | NoSuchAlgorithmException | NoSuchProviderException e) {
+ errMsg = e.toString();
+ }
+
+ try {
+ KeyFactory keyFactory = KeyFactory.getInstance(ECDSA, "BC");
+ return keyFactory.generatePrivate(keySpec);
+ } catch (InvalidKeySpecException | NoSuchAlgorithmException | NoSuchProviderException e) {
+ errMsg += "\n" + e.toString();
+ Log.e(TAG, errMsg);
+ }
+ return null;
+ }
+ }
+
+ public static String getPEMFormattedPrivateKey(PrivateKey key) throws NullPointerException {
+ if (key == null) {
+ throw new NullPointerException("Private key was null.");
+ }
+ String keyString = encodeToString(key.getEncoded(), android.util.Base64.DEFAULT);
+
+ if (key instanceof RSAPrivateKey) {
+ return (RSA_KEY_BEGIN + keyString + RSA_KEY_END);
+ } else {
+ return EC_KEY_BEGIN + keyString + EC_KEY_END;
+ }
+ }
+
+ private static PrivateKeyHelperInterface instance = new DefaultPrivateKeyHelper();
+
+ @VisibleForTesting
+ public PrivateKeyHelper(PrivateKeyHelperInterface helperInterface) {
+ if (!NativeUtils.isUnitTest()) {
+ throw new IllegalStateException("PrivateKeyHelper injected with PrivateKeyHelperInterface outside of an unit test");
+ }
+ instance = helperInterface;
+ }
+
+ public static @Nullable PrivateKey parsePrivateKeyFromString(String rsaKeyString) {
+ return instance.parsePrivateKeyFromString(rsaKeyString);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/RSAHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/RSAHelper.java
deleted file mode 100644
index 2872139a..00000000
--- a/app/src/main/java/se/leap/bitmaskclient/base/utils/RSAHelper.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package se.leap.bitmaskclient.base.utils;
-
-import android.os.Build;
-
-import androidx.annotation.VisibleForTesting;
-
-import org.spongycastle.util.encoders.Base64;
-
-import java.security.KeyFactory;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.interfaces.RSAPrivateKey;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.PKCS8EncodedKeySpec;
-
-import de.blinkt.openvpn.core.NativeUtils;
-
-public class RSAHelper {
-
- public interface RSAHelperInterface {
- RSAPrivateKey parseRsaKeyFromString(String rsaKeyString);
- }
-
- public static class DefaultRSAHelper implements RSAHelperInterface {
-
- @Override
- public RSAPrivateKey parseRsaKeyFromString(String rsaKeyString) {
- RSAPrivateKey key;
- try {
- KeyFactory kf;
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
- kf = KeyFactory.getInstance("RSA", "BC");
- } else {
- kf = KeyFactory.getInstance("RSA");
- }
- rsaKeyString = rsaKeyString.replaceFirst("-----BEGIN RSA PRIVATE KEY-----", "").replaceFirst("-----END RSA PRIVATE KEY-----", "");
- PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(rsaKeyString));
- key = (RSAPrivateKey) kf.generatePrivate(keySpec);
- } catch (InvalidKeySpecException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- return null;
- } catch (NoSuchAlgorithmException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- return null;
- } catch (NullPointerException e) {
- e.printStackTrace();
- return null;
- } catch (NoSuchProviderException e) {
- e.printStackTrace();
- return null;
- }
-
- return key;
- }
- }
-
- private static RSAHelperInterface instance = new DefaultRSAHelper();
-
- @VisibleForTesting
- public RSAHelper(RSAHelperInterface helperInterface) {
- if (!NativeUtils.isUnitTest()) {
- throw new IllegalStateException("RSAHelper injected with RSAHelperInterface outside of an unit test");
- }
- instance = helperInterface;
- }
-
- public static RSAPrivateKey parseRsaKeyFromString(String rsaKeyString) {
- return instance.parseRsaKeyFromString(rsaKeyString);
- }
-}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java b/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java
index 7aefd089..57a33bf4 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java
@@ -108,4 +108,10 @@ public class IconTextEntry extends LinearLayout {
iconView.setImageResource(id);
}
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ textView.setTextColor(getResources().getColor(enabled ? android.R.color.black : R.color.colorDisabled));
+ iconView.setImageAlpha(enabled ? 255 : 128);
+ }
}
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 ed61ca13..42935341 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java
@@ -33,9 +33,7 @@ import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_STOP_BLOCKI
import static se.leap.bitmaskclient.base.models.Constants.EIP_EARLY_ROUTES;
import static se.leap.bitmaskclient.base.models.Constants.EIP_N_CLOSEST_GATEWAY;
import static se.leap.bitmaskclient.base.models.Constants.EIP_RECEIVER;
-import static se.leap.bitmaskclient.base.models.Constants.EIP_RESTART_ON_BOOT;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PROFILE;
-import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.base.utils.ConfigHelper.ensureNotOnMainThread;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getPreferredCity;
import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_INVALID_PROFILE;
@@ -71,8 +69,6 @@ import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.Closeable;
import java.lang.ref.WeakReference;
-import java.util.Observable;
-import java.util.Observer;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
@@ -87,7 +83,6 @@ import se.leap.bitmaskclient.base.OnBootReceiver;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.base.models.ProviderObservable;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
-import se.leap.bitmaskclient.eip.GatewaysManager.GatewayOptions;
/**
* EIP is the abstract base class for interacting with and managing the Encrypted
@@ -251,8 +246,8 @@ public final class EIP extends JobIntentService implements PropertyChangeListene
return;
}
- GatewayOptions gatewayOptions = gatewaysManager.select(nClosestGateway);
- launchActiveGateway(gatewayOptions, nClosestGateway, result);
+ VpnProfile gatewayOptions = gatewaysManager.selectVpnProfile(nClosestGateway);
+ launchProfile(gatewayOptions, nClosestGateway, result);
if (result.containsKey(BROADCAST_RESULT_KEY) && !result.getBoolean(BROADCAST_RESULT_KEY)) {
tellToReceiverOrBroadcast(this, EIP_ACTION_START, RESULT_CANCELED, result);
} else {
@@ -266,7 +261,7 @@ public final class EIP extends JobIntentService implements PropertyChangeListene
*/
private void startEIPAlwaysOnVpn() {
GatewaysManager gatewaysManager = new GatewaysManager(getApplicationContext());
- GatewayOptions gatewayOptions = gatewaysManager.select(0);
+ VpnProfile vpnProfile = gatewaysManager.selectVpnProfile(0);
Bundle result = new Bundle();
if (shouldUpdateVPNCertificate()) {
@@ -274,8 +269,7 @@ public final class EIP extends JobIntentService implements PropertyChangeListene
p.setShouldUpdateVpnCertificate(true);
ProviderObservable.getInstance().updateProvider(p);
}
-
- launchActiveGateway(gatewayOptions, 0, result);
+ launchProfile(vpnProfile, 0, result);
if (result.containsKey(BROADCAST_RESULT_KEY) && !result.getBoolean(BROADCAST_RESULT_KEY)){
VpnStatus.logWarning("ALWAYS-ON VPN: " + getString(R.string.no_vpn_profiles_defined));
}
@@ -317,15 +311,15 @@ public final class EIP extends JobIntentService implements PropertyChangeListene
}
/**
- * starts the VPN and connects to the given gateway
+ * starts the VPN and connects to the given Gateway the VpnProfile belongs to
*
- * @param gatewayOptions GatewayOptions model containing a Gateway and the associated transport used to connect
+ * @param profile VpnProfile which contains all information to setup a OpenVPN connection
+ * and optionally obfsvpn
+ * @param nClosestGateway gateway index, indicating the distance to the user
+ * @param result Bundle containing possible error messages shown to the user
*/
- private void launchActiveGateway(@Nullable GatewayOptions gatewayOptions, int nClosestGateway, Bundle result) {
- VpnProfile profile;
-
- if (gatewayOptions == null || gatewayOptions.gateway == null ||
- (profile = gatewayOptions.gateway.getProfile(gatewayOptions.transportType)) == null) {
+ private void launchProfile(@Nullable VpnProfile profile, int nClosestGateway, Bundle result) {
+ if (profile == null) {
String preferredLocation = getPreferredCity();
if (preferredLocation != null) {
setErrorResult(result, NO_MORE_GATEWAYS.toString(), getStringResourceForNoMoreGateways(), getString(R.string.app_name), preferredLocation);
@@ -379,7 +373,6 @@ public final class EIP extends JobIntentService implements PropertyChangeListene
}
}
-
/**
* Stop VPN
* First checks if the OpenVpnConnection is open then
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java b/app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java
index ed95b75c..31933717 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java
@@ -27,6 +27,7 @@ import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_GATEWAY_SETU
import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_PROVIDER_API_EVENT;
import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_CODE;
import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.COUNTRYCODE;
import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_LAUNCH_VPN;
import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_PREPARE_VPN;
import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START;
@@ -60,6 +61,7 @@ import android.content.IntentFilter;
import android.os.Bundle;
import android.util.Log;
+import androidx.core.os.BundleCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.json.JSONObject;
@@ -81,7 +83,6 @@ import se.leap.bitmaskclient.base.utils.PreferenceHelper;
import se.leap.bitmaskclient.providersetup.ProviderAPI;
import se.leap.bitmaskclient.providersetup.ProviderAPICommand;
import se.leap.bitmaskclient.providersetup.ProviderSetupObservable;
-import se.leap.bitmaskclient.providersetup.activities.SetupActivity;
import se.leap.bitmaskclient.tor.TorServiceCommand;
import se.leap.bitmaskclient.tor.TorStatusObservable;
@@ -201,7 +202,7 @@ public class EipSetupObserver extends BroadcastReceiver implements VpnStatus.Sta
switch (resultCode) {
case CORRECTLY_DOWNLOADED_EIP_SERVICE:
Log.d(TAG, "correctly updated service json");
- provider = resultData.getParcelable(PROVIDER_KEY);
+ provider = BundleCompat.getParcelable(resultData, PROVIDER_KEY, Provider.class);
ProviderObservable.getInstance().updateProvider(provider);
PreferenceHelper.storeProviderInPreferences(provider);
if (EipStatus.getInstance().isDisconnected()) {
@@ -209,7 +210,7 @@ public class EipSetupObserver extends BroadcastReceiver implements VpnStatus.Sta
}
break;
case CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE:
- provider = resultData.getParcelable(PROVIDER_KEY);
+ provider = BundleCompat.getParcelable(resultData, PROVIDER_KEY, Provider.class);
ProviderObservable.getInstance().updateProvider(provider);
PreferenceHelper.storeProviderInPreferences(provider);
EipCommand.startVPN(appContext, false);
@@ -219,7 +220,7 @@ public class EipSetupObserver extends BroadcastReceiver implements VpnStatus.Sta
}
break;
case CORRECTLY_DOWNLOADED_GEOIP_JSON:
- provider = resultData.getParcelable(PROVIDER_KEY);
+ provider = BundleCompat.getParcelable(resultData, PROVIDER_KEY, Provider.class);
ProviderObservable.getInstance().updateProvider(provider);
PreferenceHelper.storeProviderInPreferences(provider);
maybeStartEipService(resultData);
@@ -245,10 +246,8 @@ public class EipSetupObserver extends BroadcastReceiver implements VpnStatus.Sta
Log.d(TAG, "PROVIDER OK - FETCH SUCCESSFUL");
//no break, continue with next case
case CORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
- if (ProviderSetupObservable.getProgress() > 0 && !activityForeground.get()) {
- Intent activityIntent = new Intent(appContext, SetupActivity.class);
- activityIntent.setAction(Intent.ACTION_MAIN);
- appContext.startActivity(activityIntent);
+ if (ProviderSetupObservable.isSetupRunning() && !activityForeground.get()) {
+ ProviderSetupObservable.storeLastResult(resultCode, resultData);
}
break;
case TOR_TIMEOUT:
@@ -270,6 +269,8 @@ public class EipSetupObserver extends BroadcastReceiver implements VpnStatus.Sta
for (EipSetupListener listener : listeners) {
listener.handleProviderApiEvent(intent);
}
+
+
}
private void maybeStartEipService(Bundle resultData) {
@@ -387,6 +388,7 @@ public class EipSetupObserver extends BroadcastReceiver implements VpnStatus.Sta
//setupNClostestGateway > 0: at least one failed gateway -> did the provider change it's gateways?
Bundle parameters = new Bundle();
parameters.putLong(DELAY, 500);
+ parameters.putString(COUNTRYCODE, PreferenceHelper.getBaseCountry());
ProviderAPICommand.execute(appContext, ProviderAPI.DOWNLOAD_SERVICE_JSON, parameters, provider);
}
@@ -451,22 +453,6 @@ public class EipSetupObserver extends BroadcastReceiver implements VpnStatus.Sta
if (BuildConfig.DEBUG) {
Log.e("ERROR", logItem.getString(appContext));
}
- switch (logItem.getErrorType()) {
- case SHAPESHIFTER:
- VpnProfile profile = VpnStatus.getLastConnectedVpnProfile();
- if (profile == null) {
- EipCommand.startVPN(appContext, false, 0);
- } else {
- GatewaysManager gatewaysManager = new GatewaysManager(appContext);
- int position = gatewaysManager.getPosition(profile);
- setupNClosestGateway.set(position >= 0 ? position : 0);
- selectNextGateway();
- }
- break;
- default:
- break;
-
- }
}
}
}
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 d2592cd7..783f9124 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java
@@ -20,6 +20,7 @@ import static de.blinkt.openvpn.core.connection.Connection.TransportType.PT;
import static se.leap.bitmaskclient.base.models.Constants.FULLNESS;
import static se.leap.bitmaskclient.base.models.Constants.HOST;
import static se.leap.bitmaskclient.base.models.Constants.IP_ADDRESS;
+import static se.leap.bitmaskclient.base.models.Constants.IP_ADDRESS6;
import static se.leap.bitmaskclient.base.models.Constants.LOCATION;
import static se.leap.bitmaskclient.base.models.Constants.LOCATIONS;
import static se.leap.bitmaskclient.base.models.Constants.NAME;
@@ -27,17 +28,10 @@ import static se.leap.bitmaskclient.base.models.Constants.OPENVPN_CONFIGURATION;
import static se.leap.bitmaskclient.base.models.Constants.OVERLOAD;
import static se.leap.bitmaskclient.base.models.Constants.TIMEZONE;
import static se.leap.bitmaskclient.base.models.Constants.VERSION;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.allowExperimentalTransports;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getExcludedApps;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getObfuscationPinningCert;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getObfuscationPinningGatewayLocation;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getObfuscationPinningIP;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getObfuscationPinningKCP;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getObfuscationPinningPort;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getPreferUDP;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useObfuscationPinning;
+import static se.leap.bitmaskclient.base.models.Transport.createTransportsFrom;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.google.gson.Gson;
@@ -45,12 +39,20 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
-import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.Vector;
import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.ConfigParser;
import de.blinkt.openvpn.core.connection.Connection;
+import io.swagger.client.model.ModelsBridge;
+import io.swagger.client.model.ModelsEIPService;
+import io.swagger.client.model.ModelsGateway;
+import io.swagger.client.model.ModelsLocation;
+import se.leap.bitmaskclient.base.models.Transport;
import se.leap.bitmaskclient.base.utils.ConfigHelper;
/**
@@ -69,16 +71,19 @@ public class Gateway {
private JSONObject generalConfiguration;
private JSONObject secrets;
private JSONObject gateway;
+ private Vector<ModelsGateway> modelsGateways;
+ private Vector<ModelsBridge> modelsBridges;
private JSONObject load;
// the location of a gateway is its name
private String name;
private int timezone;
private int apiVersion;
- /** FIXME: We expect here that not more than one obfs4 transport is offered by a gateway, however
- * it's possible to setup gateways that have obfs4 over kcp and tcp which result in different VpnProfiles each
- */
- private HashMap<Connection.TransportType, VpnProfile> vpnProfiles;
+ private Vector<VpnProfile> vpnProfiles;
+ private String remoteIpAddress;
+ private String remoteIpAddressV6;
+ private String host;
+ private String locationName;
/**
* Build a gateway object from a JSON OpenVPN gateway definition in eip-service.json
@@ -95,32 +100,69 @@ public class Gateway {
this.gateway = gateway;
this.secrets = secrets;
this.load = load;
+ this.apiVersion = eipDefinition.optInt(VERSION);
+ this.remoteIpAddress = gateway.optString(IP_ADDRESS);
+ this.remoteIpAddressV6 = gateway.optString(IP_ADDRESS6);
+ this.host = gateway.optString(HOST);
+ JSONObject location = getLocationInfo(gateway, eipDefinition);
+ this.locationName = location.optString(NAME);
+ this.timezone = location.optInt(TIMEZONE);
+ VpnConfigGenerator.Configuration configuration = getProfileConfig(Transport.createTransportsFrom(gateway, apiVersion));
+ this.generalConfiguration = getGeneralConfiguration(eipDefinition);
+ this.name = configuration.profileName;
+ this.vpnProfiles = createVPNProfiles(configuration);
+ }
+
+
+ public Gateway(ModelsEIPService eipService, JSONObject secrets, ModelsGateway modelsGateway, int apiVersion) throws ConfigParser.ConfigParseError, NumberFormatException, JSONException, IOException {
+ this.apiVersion = apiVersion;
+ generalConfiguration = getGeneralConfiguration(eipService);
+ this.secrets = secrets;
+ this.modelsGateways = new Vector<>();
+ this.modelsBridges = new Vector<>();
+ this.modelsGateways.add(modelsGateway);
+
+ this.remoteIpAddress = modelsGateway.getIpAddr();
+ this.remoteIpAddressV6 = modelsGateway.getIp6Addr();
+ this.host = modelsGateway.getHost();
+ ModelsLocation modelsLocation = eipService.getLocations().get(modelsGateway.getLocation());
+ if (modelsLocation != null) {
+ this.locationName = modelsLocation.getDisplayName();
+ this.timezone = Integer.parseInt(modelsLocation.getTimezone());
+ } else {
+ this.locationName = modelsGateway.getLocation();
+ }
+ this.apiVersion = apiVersion;
+ VpnConfigGenerator.Configuration configuration = getProfileConfig(createTransportsFrom(modelsGateway));
+ this.name = configuration.profileName;
+ this.vpnProfiles = createVPNProfiles(configuration);
+ }
- apiVersion = getApiVersion(eipDefinition);
- VpnConfigGenerator.Configuration configuration = getProfileConfig(eipDefinition, apiVersion);
- generalConfiguration = getGeneralConfiguration(eipDefinition);
- timezone = getTimezone(eipDefinition);
+ public Gateway(ModelsEIPService eipService, JSONObject secrets, ModelsBridge modelsBridge, int apiVersion) throws ConfigParser.ConfigParseError, JSONException, IOException {
+ this.apiVersion = apiVersion;
+ generalConfiguration = getGeneralConfiguration(eipService);
+ this.secrets = secrets;
+ this.modelsGateways = new Vector<>();
+ this.modelsBridges = new Vector<>();
+ this.modelsBridges.add(modelsBridge);
+ remoteIpAddress = modelsBridge.getIpAddr();
+ host = modelsBridge.getHost();
+ ModelsLocation modelsLocation = eipService.getLocations().get(modelsBridge.getLocation());
+ if (modelsLocation != null) {
+ this.locationName = modelsLocation.getDisplayName();
+ this.timezone = Integer.parseInt(modelsLocation.getTimezone());
+ } else {
+ this.locationName = modelsBridge.getLocation();
+ } this.apiVersion = apiVersion;
+ VpnConfigGenerator.Configuration configuration = getProfileConfig(Transport.createTransportsFrom(modelsBridge));
name = configuration.profileName;
vpnProfiles = createVPNProfiles(configuration);
}
- private VpnConfigGenerator.Configuration getProfileConfig(JSONObject eipDefinition, int apiVersion) {
- VpnConfigGenerator.Configuration config = new VpnConfigGenerator.Configuration();
- config.apiVersion = apiVersion;
- config.preferUDP = getPreferUDP();
- config.experimentalTransports = allowExperimentalTransports();
- config.excludedApps = getExcludedApps();
-
- config.remoteGatewayIP = config.useObfuscationPinning ? getObfuscationPinningIP() : gateway.optString(IP_ADDRESS);
- config.useObfuscationPinning = useObfuscationPinning();
- config.profileName = config.useObfuscationPinning ? getObfuscationPinningGatewayLocation() : locationAsName(eipDefinition);
- if (config.useObfuscationPinning) {
- config.obfuscationProxyIP = getObfuscationPinningIP();
- config.obfuscationProxyPort = getObfuscationPinningPort();
- config.obfuscationProxyCert = getObfuscationPinningCert();
- config.obfuscationProxyKCP = getObfuscationPinningKCP();
- }
- return config;
+
+
+ private VpnConfigGenerator.Configuration getProfileConfig(Vector<Transport> transports) {
+ return VpnConfigGenerator.Configuration.createProfileConfig(transports, apiVersion, remoteIpAddress, remoteIpAddressV6, locationName);
}
public void updateLoad(JSONObject load) {
@@ -135,29 +177,33 @@ public class Gateway {
}
}
- private int getTimezone(JSONObject eipDefinition) {
- JSONObject location = getLocationInfo(eipDefinition);
- return location.optInt(TIMEZONE);
- }
+ private JSONObject getGeneralConfiguration(ModelsEIPService eipService) {
+ JSONObject config = new JSONObject();
+ Map<String, Object> openvpnOptions = eipService.getOpenvpnConfiguration();
+ Set<String> keys = openvpnOptions.keySet();
+ Iterator<String> i = keys.iterator();
+ while (i.hasNext()) {
+ try {
+ String key = i.next();
+ Object o = openvpnOptions.get(key);
+ config.put(key, o);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
- private int getApiVersion(JSONObject eipDefinition) {
- return eipDefinition.optInt(VERSION);
+ return config;
}
public String getRemoteIP() {
- return gateway.optString(IP_ADDRESS);
+ return remoteIpAddress;
}
public String getHost() {
- return gateway.optString(HOST);
+ return host;
}
- private String locationAsName(JSONObject eipDefinition) {
- JSONObject location = getLocationInfo(eipDefinition);
- return location.optString(NAME);
- }
-
- private JSONObject getLocationInfo(JSONObject eipDefinition) {
+ private JSONObject getLocationInfo(JSONObject gateway, JSONObject eipDefinition) {
try {
JSONObject locations = eipDefinition.getJSONObject(LOCATIONS);
@@ -190,39 +236,78 @@ public class Gateway {
/**
* Create and attach the VpnProfile to our gateway object
*/
- private @NonNull HashMap<Connection.TransportType, VpnProfile> createVPNProfiles(VpnConfigGenerator.Configuration profileConfig)
+ private @NonNull Vector<VpnProfile> createVPNProfiles(VpnConfigGenerator.Configuration profileConfig)
throws ConfigParser.ConfigParseError, IOException, JSONException {
- VpnConfigGenerator vpnConfigurationGenerator = new VpnConfigGenerator(generalConfiguration, secrets, gateway, profileConfig);
- HashMap<Connection.TransportType, VpnProfile> profiles = vpnConfigurationGenerator.generateVpnProfiles();
- return profiles;
+ VpnConfigGenerator vpnConfigurationGenerator = new VpnConfigGenerator(generalConfiguration, secrets, profileConfig);
+ return vpnConfigurationGenerator.generateVpnProfiles();
}
public String getName() {
return name;
}
- public HashMap<Connection.TransportType, VpnProfile> getProfiles() {
+ public Vector<VpnProfile> getProfiles() {
return vpnProfiles;
}
- public VpnProfile getProfile(Connection.TransportType transportType) {
- return vpnProfiles.get(transportType);
+ /**
+ * Returns a VpnProfile that supports a given transport type and any of the given transport
+ * layer protocols (e.g. TCP, KCP). If multiple VpnProfiles fulfill these requirements, a random
+ * profile will be chosen. This can currently only occur for obfuscation protocols.
+ * @param transportType transport type, e.g. openvpn or obfs4
+ * @param obfuscationTransportLayerProtocols Vector of transport layer protocols PTs can be based on
+ * @return
+ */
+ public @Nullable VpnProfile getProfile(Connection.TransportType transportType, @Nullable Set<String> obfuscationTransportLayerProtocols) {
+ Vector<VpnProfile> results = new Vector<>();
+ for (VpnProfile vpnProfile : vpnProfiles) {
+ if (vpnProfile.getTransportType() == transportType) {
+ if (!vpnProfile.usePluggableTransports() ||
+ obfuscationTransportLayerProtocols == null ||
+ obfuscationTransportLayerProtocols.contains(vpnProfile.getObfuscationTransportLayerProtocol())) {
+ results.add(vpnProfile);
+ }
+ }
+ }
+ if (results.size() == 0) {
+ return null;
+ }
+ int randomIndex = (int) (Math.random() * (results.size()));
+ return results.get(randomIndex);
}
- public boolean supportsTransport(Connection.TransportType transportType) {
+ public boolean hasProfile(VpnProfile profile) {
+ return vpnProfiles.contains(profile);
+ }
+
+ /**
+ * Checks if a transport type is supported by the gateway.
+ * In case the transport type is an obfuscation transport, you can pass a Vector of required transport layer protocols.
+ * This way you can filter for TCP based obfs4 traffic versus KCP based obfs4 traffic.
+ * @param transportType transport type, e.g. openvpn or obfs4
+ * @param obfuscationTransportLayerProtocols filters for _any_ of these transport layer protocols (e.g. TCP, KCP, QUIC) of a given obfuscation transportType, can be omitted if transportType is OPENVPN.
+ *
+ * @return
+ */
+ public boolean supportsTransport(Connection.TransportType transportType, @Nullable Set<String> obfuscationTransportLayerProtocols) {
if (transportType == PT) {
return supportsPluggableTransports();
}
- return vpnProfiles.get(transportType) != null;
+ return getProfile(transportType, obfuscationTransportLayerProtocols) != null;
}
- public HashSet<Connection.TransportType> getSupportedTransports() {
- return new HashSet<>(vpnProfiles.keySet());
+ public Set<Connection.TransportType> getSupportedTransports() {
+ Set<Connection.TransportType> transportTypes = new HashSet<>();
+ for (VpnProfile p : vpnProfiles) {
+ transportTypes.add(p.getTransportType());
+ }
+ return transportTypes;
}
public boolean supportsPluggableTransports() {
- for (Connection.TransportType transportType : vpnProfiles.keySet()) {
- if (transportType.isPluggableTransport() && vpnProfiles.get(transportType) != null) {
+ for (VpnProfile profile : vpnProfiles) {
+ Connection.TransportType transportType = profile.getTransportType();
+ if (transportType.isPluggableTransport()) {
return true;
}
}
@@ -238,4 +323,16 @@ public class Gateway {
return new Gson().toJson(this, Gateway.class);
}
+ public Gateway addTransport(Transport transport) {
+ Vector<Transport> transports = new Vector<>();
+ transports.add(transport);
+ VpnConfigGenerator.Configuration profileConfig = getProfileConfig(transports);
+ try {
+ Vector<VpnProfile> profiles = createVPNProfiles(profileConfig);
+ vpnProfiles.addAll(profiles);
+ } catch (ConfigParser.ConfigParseError | IOException | JSONException e) {
+ e.printStackTrace();
+ }
+ return this;
+ }
}
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 9b4d431c..b207fb14 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java
@@ -20,16 +20,25 @@ import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_HOP;
import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN;
import static de.blinkt.openvpn.core.connection.Connection.TransportType.PT;
+import static se.leap.bitmaskclient.base.models.Constants.CERT;
import static se.leap.bitmaskclient.base.models.Constants.GATEWAYS;
import static se.leap.bitmaskclient.base.models.Constants.HOST;
+import static se.leap.bitmaskclient.base.models.Constants.IAT_MODE;
+import static se.leap.bitmaskclient.base.models.Constants.KCP;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.base.models.Constants.QUIC;
import static se.leap.bitmaskclient.base.models.Constants.SORTED_GATEWAYS;
+import static se.leap.bitmaskclient.base.models.Constants.TCP;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getObfuscationPinningCert;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getObfuscationPinningIP;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getObfuscationPinningKCP;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getObfuscationPinningPort;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getObfuscationPinningProtocol;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getPreferredCity;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseBridges;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseObfs4;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseObfs4Kcp;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseObfs4Quic;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUsePortHopping;
import android.content.Context;
import android.util.Log;
@@ -50,12 +59,16 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Set;
import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.ConfigParser;
import de.blinkt.openvpn.core.VpnStatus;
import de.blinkt.openvpn.core.connection.Connection;
import de.blinkt.openvpn.core.connection.Connection.TransportType;
+import io.swagger.client.model.ModelsBridge;
+import io.swagger.client.model.ModelsEIPService;
+import io.swagger.client.model.ModelsGateway;
import se.leap.bitmaskclient.BuildConfig;
import se.leap.bitmaskclient.R;
import se.leap.bitmaskclient.base.models.GatewayJson;
@@ -101,16 +114,6 @@ public class GatewaysManager {
}
}
- public static class GatewayOptions {
- public Gateway gateway;
- public TransportType transportType;
-
- public GatewayOptions(Gateway gateway, TransportType transportType) {
- this.gateway = gateway;
- this.transportType = transportType;
- }
- }
-
private static final String TAG = GatewaysManager.class.getSimpleName();
public static final String PINNED_OBFUSCATION_PROXY = "pinned.obfuscation.proxy";
@@ -130,12 +133,12 @@ public class GatewaysManager {
}
/**
- * select closest Gateway
- * @return the n closest Gateway
+ * selects a VpnProfile of the n closest Gateway or a pinned gateway
+ * @return VpnProfile of the n closest Gateway or null if no remaining VpnProfiles available
*/
- public GatewayOptions select(int nClosest) {
+ public @Nullable VpnProfile selectVpnProfile(int nClosestGateway) {
if (PreferenceHelper.useObfuscationPinning()) {
- if (nClosest > 2) {
+ if (nClosestGateway > 2) {
// no need to try again the pinned proxy, probably configuration error
return null;
}
@@ -143,19 +146,61 @@ public class GatewaysManager {
if (gateway == null) {
return null;
}
- return new GatewayOptions(gateway, OBFS4);
+ return gateway.getProfile(OBFS4, null);
}
String selectedCity = getPreferredCity();
- return select(nClosest, selectedCity);
+ return selectVpnProfile(nClosestGateway, selectedCity);
}
- public GatewayOptions select(int nClosest, String city) {
- TransportType[] transportTypes = getUseBridges() ? new TransportType[]{OBFS4, OBFS4_HOP} : new TransportType[]{OPENVPN};
+ /**
+ * Selects a VPN profile, filtered by distance to the user, transportType and
+ * optionally by city and transport layer protocol
+ * @param nClosestGateway
+ * @param city location filter
+ * @return VpnProfile of the n closest Gateway or null if no remaining VpnProfiles available
+ */
+ public @Nullable VpnProfile selectVpnProfile(int nClosestGateway, String city) {
+ TransportType[] transportTypes = determineTransportTypes();
+ Set<String> obfuscationTransportLayerProtocols = getObfuscationTransportLayerProtocols();
if (presortedList.size() > 0) {
- return getGatewayFromPresortedList(nClosest, transportTypes, city);
+ return getVpnProfileFromPresortedList(nClosestGateway, transportTypes, obfuscationTransportLayerProtocols, city);
}
- return getGatewayFromTimezoneCalculation(nClosest, transportTypes, city);
+ return getVpnProfileFromTimezoneCalculation(nClosestGateway, transportTypes, obfuscationTransportLayerProtocols, city);
+ }
+
+ private TransportType[] determineTransportTypes() {
+ if (!getUseBridges()){
+ return new TransportType[]{OPENVPN};
+ }
+
+ if (getUsePortHopping()) {
+ return new TransportType[]{OBFS4_HOP};
+ } else if (getUseObfs4() || getUseObfs4Kcp() || getUseObfs4Quic()) {
+ return new TransportType[]{OBFS4};
+ } else {
+ return new TransportType[]{OBFS4, OBFS4_HOP};
+ }
+ }
+
+
+ @Nullable
+ private static Set<String> getObfuscationTransportLayerProtocols() {
+ if (!getUseBridges()) {
+ return null;
+ }
+
+ if (getUseObfs4()) {
+ return Set.of(TCP);
+ } else if (getUseObfs4Kcp()) {
+ return Set.of(KCP);
+ } else if (getUseObfs4Quic()) {
+ return Set.of(QUIC);
+ } else {
+ // If neither Obf4 nor Obf4Kcp are used, and bridges are enabled,
+ // then allow to use any of these protocols
+ return Set.of(TCP, KCP, QUIC);
+ }
}
public void updateTransport(TransportType transportType) {
@@ -239,7 +284,7 @@ public class GatewaysManager {
}
private void updateLocation(Location location, Gateway gateway, Connection.TransportType transportType) {
- if (gateway.supportsTransport(transportType)) {
+ if (gateway.supportsTransport(transportType, null)) {
double averageLoad = location.getAverageLoad(transportType);
int numberOfGateways = location.getNumberOfGateways(transportType);
averageLoad = (numberOfGateways * averageLoad + gateway.getFullness()) / (numberOfGateways + 1);
@@ -269,6 +314,15 @@ public class GatewaysManager {
return null;
}
+ public boolean hasLocationsForOpenVPN() {
+ for (Gateway gateway : gateways.values()) {
+ if (gateway.supportsTransport(OPENVPN, null)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
public Load getLoadForLocation(@Nullable String name, TransportType transportType) {
Location location = getLocation(name);
if (location == null) {
@@ -277,7 +331,7 @@ public class GatewaysManager {
return Load.getLoadByValue(location.getAverageLoad(transportType));
}
- private GatewayOptions getGatewayFromTimezoneCalculation(int nClosest, TransportType[] transportTypes, @Nullable String city) {
+ private VpnProfile getVpnProfileFromTimezoneCalculation(int nClosest, TransportType[] transportTypes, @Nullable Set<String> protocols, @Nullable String city) {
List<Gateway> list = new ArrayList<>(gateways.values());
if (gatewaySelector == null) {
gatewaySelector = new GatewaySelector(list);
@@ -287,10 +341,10 @@ public class GatewaysManager {
int i = 0;
while ((gateway = gatewaySelector.select(i)) != null) {
for (TransportType transportType : transportTypes) {
- if ((city == null && gateway.supportsTransport(transportType)) ||
- (gateway.getName().equals(city) && gateway.supportsTransport(transportType))) {
+ if ((city == null && gateway.supportsTransport(transportType, protocols)) ||
+ (gateway.getName().equals(city) && gateway.supportsTransport(transportType, protocols))) {
if (found == nClosest) {
- return new GatewayOptions(gateway, transportType);
+ return gateway.getProfile(transportType, protocols);
}
found++;
}
@@ -300,19 +354,18 @@ public class GatewaysManager {
return null;
}
- private GatewayOptions getGatewayFromPresortedList(int nClosest, TransportType[] transportTypes, @Nullable String city) {
+ private VpnProfile getVpnProfileFromPresortedList(int nClosest, TransportType[] transportTypes, @Nullable Set<String> protocols, @Nullable String city) {
int found = 0;
for (Gateway gateway : presortedList) {
for (TransportType transportType : transportTypes) {
- if ((city == null && gateway.supportsTransport(transportType)) ||
- (gateway.getName().equals(city) && gateway.supportsTransport(transportType))) {
+ if ((city == null && gateway.supportsTransport(transportType, protocols)) ||
+ (gateway.getName().equals(city) && gateway.supportsTransport(transportType, protocols))) {
if (found == nClosest) {
- return new GatewayOptions(gateway, transportType);
+ return gateway.getProfile(transportType, protocols);
}
found++;
}
}
-
}
return null;
}
@@ -323,43 +376,36 @@ public class GatewaysManager {
* @return position of the gateway owning to the profile
*/
public int getPosition(VpnProfile profile) {
- if (presortedList.size() > 0) {
+ if (presortedList.size() > 0) {
return getPositionFromPresortedList(profile);
- }
-
+ }
+
return getPositionFromTimezoneCalculatedList(profile);
}
-
+
private int getPositionFromPresortedList(VpnProfile profile) {
- TransportType transportType = profile.getTransportType();
- int nClosest = 0;
+ int nClosestGateway = 0;
for (Gateway gateway : presortedList) {
- if (gateway.supportsTransport(transportType)) {
- if (profile.equals(gateway.getProfile(transportType))) {
- return nClosest;
- }
- nClosest++;
+ if (gateway.hasProfile(profile)) {
+ return nClosestGateway;
}
+ nClosestGateway++;
}
return -1;
}
private int getPositionFromTimezoneCalculatedList(VpnProfile profile) {
- TransportType transportType = profile.getTransportType();
if (gatewaySelector == null) {
gatewaySelector = new GatewaySelector(new ArrayList<>(gateways.values()));
}
Gateway gateway;
- int nClosest = 0;
+ int nClosestGateway = 0;
int i = 0;
- while ((gateway = gatewaySelector.select(i)) != null) {
- if (gateway.supportsTransport(transportType)) {
- if (profile.equals(gateway.getProfile(transportType))) {
- return nClosest;
- }
- nClosest++;
+ while ((gateway = gatewaySelector.select(nClosestGateway)) != null) {
+ if (gateway.hasProfile(profile)) {
+ return nClosestGateway;
}
- i++;
+ nClosestGateway++;
}
return -1;
}
@@ -384,84 +430,150 @@ public class GatewaysManager {
return new Gson().toJson(gateways, listType);
}
- /**
- * parse gateways from Provider's eip service
- * @param provider
- */
- private void parseDefaultGateways(Provider provider) {
- try {
- JSONObject eipDefinition = provider.getEipServiceJson();
- JSONObject secrets = secretsConfigurationFromCurrentProvider();
- JSONArray gatewaysDefined = new JSONArray();
- try {
- gatewaysDefined = eipDefinition.getJSONArray(GATEWAYS);
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- if (PreferenceHelper.useObfuscationPinning()) {
- try {
- Transport[] transports = new Transport[]{
- new Transport(OBFS4.toString(),
- new String[]{getObfuscationPinningKCP() ? "kcp" : "tcp"},
- new String[]{getObfuscationPinningPort()},
- getObfuscationPinningCert())};
- GatewayJson.Capabilities capabilities = new GatewayJson.Capabilities(false, false, false, transports, false);
- GatewayJson gatewayJson = new GatewayJson(context.getString(R.string.unknown_location), getObfuscationPinningIP(
-
- ), null, PINNED_OBFUSCATION_PROXY, capabilities);
- Gateway gateway = new Gateway(eipDefinition, secrets, new JSONObject(gatewayJson.toString()));
- addGateway(gateway);
- } catch (JSONException | ConfigParser.ConfigParseError | IOException e) {
- e.printStackTrace();
- }
- } else {
- for (int i = 0; i < gatewaysDefined.length(); i++) {
- try {
- JSONObject gw = gatewaysDefined.getJSONObject(i);
- Gateway aux = new Gateway(eipDefinition, secrets, gw);
- if (gateways.get(aux.getHost()) == null) {
- addGateway(aux);
- }
- } catch (JSONException | IOException e) {
- e.printStackTrace();
- VpnStatus.logError("Unable to parse gateway config!");
- } catch (ConfigParser.ConfigParseError e) {
- VpnStatus.logError("Unable to parse gateway config: " + e.getLocalizedMessage());
- }
- }
- }
- } catch (NullPointerException npe) {
- npe.printStackTrace();
- }
+ public void parseGatewaysV3(Provider provider) {
+ try {
+ JSONObject eipDefinition = provider.getEipServiceJson();
+ JSONObject secrets = secretsConfigurationFromCurrentProvider();
+ JSONArray gatewaysDefined = new JSONArray();
+ try {
+ gatewaysDefined = eipDefinition.getJSONArray(GATEWAYS);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ if (PreferenceHelper.useObfuscationPinning()) {
+ try {
+ Transport[] transports = new Transport[]{
+ new Transport(OBFS4.toString(),
+ new String[]{getObfuscationPinningProtocol()},
+ new String[]{getObfuscationPinningPort()},
+ getObfuscationPinningCert())};
+ GatewayJson.Capabilities capabilities = new GatewayJson.Capabilities(false, false, false, transports, false);
+ GatewayJson gatewayJson = new GatewayJson(context.getString(R.string.unknown_location), getObfuscationPinningIP(
+
+ ), null, PINNED_OBFUSCATION_PROXY, capabilities);
+ Gateway gateway = new Gateway(eipDefinition, secrets, new JSONObject(gatewayJson.toString()));
+ addGateway(gateway);
+ } catch (JSONException | ConfigParser.ConfigParseError | IOException e) {
+ e.printStackTrace();
+ }
+ } else {
+ for (int i = 0; i < gatewaysDefined.length(); i++) {
+ try {
+ JSONObject gw = gatewaysDefined.getJSONObject(i);
+ Gateway aux = new Gateway(eipDefinition, secrets, gw);
+ if (gateways.get(aux.getHost()) == null) {
+ addGateway(aux);
+ }
+ } catch (JSONException | IOException e) {
+ e.printStackTrace();
+ VpnStatus.logError("Unable to parse gateway config!");
+ } catch (ConfigParser.ConfigParseError e) {
+ VpnStatus.logError("Unable to parse gateway config: " + e.getLocalizedMessage());
+ }
+ }
+ }
+ } catch (NullPointerException npe) {
+ npe.printStackTrace();
+ }
+
+ if (BuildConfig.BUILD_TYPE.equals("debug") && handleGatewayPinning()) {
+ return;
+ }
+
+ // parse v3 menshen geoIP json variants
+ if (hasSortedGatewaysWithLoad(provider)) {
+ parseGatewaysWithLoad(provider);
+ } else {
+ parseSimpleGatewayList(provider);
+ }
+ }
+
+ public void parseGatewaysV5(Provider provider) {
+ ModelsGateway[] modelsGateways = provider.getGateways();
+ ModelsBridge[] modelsBridges = provider.getBridges();
+ ModelsEIPService modelsEIPService = provider.getService();
+ JSONObject secrets = secretsConfigurationFromCurrentProvider();
+ int apiVersion = provider.getApiVersion();
+
+ if (PreferenceHelper.useObfuscationPinning()) {
+ try {
+ ModelsBridge modelsBridge = new ModelsBridge();
+ modelsBridge.ipAddr(getObfuscationPinningIP());
+ modelsBridge.port(Integer.valueOf(getObfuscationPinningPort()));
+ HashMap<String, Object> options = new HashMap<>();
+ options.put(CERT, getObfuscationPinningCert());
+ options.put(IAT_MODE, "0");
+ modelsBridge.options(options);
+ modelsBridge.transport(getObfuscationPinningProtocol());
+ modelsBridge.type(OBFS4.toString());
+ modelsBridge.host(PINNED_OBFUSCATION_PROXY);
+ Gateway gateway = new Gateway(modelsEIPService, secrets, modelsBridge, provider.getApiVersion());
+ addGateway(gateway);
+ } catch (NumberFormatException | ConfigParser.ConfigParseError | JSONException |
+ IOException e) {
+ e.printStackTrace();
+ }
+ } else {
+ for (ModelsGateway modelsGateway : modelsGateways) {
+ String host = modelsGateway.getHost();
+ Gateway gateway = gateways.get(host);
+ if (gateway == null) {
+ try {
+ addGateway(new Gateway(modelsEIPService, secrets, modelsGateway, apiVersion));
+ } catch (ConfigParser.ConfigParseError | JSONException | IOException e) {
+ e.printStackTrace();
+ }
+ } else {
+ addGateway(gateway.addTransport(Transport.createTransportFrom(modelsGateway)));
+ }
+ }
+ for (ModelsBridge modelsBridge : modelsBridges) {
+ String host = modelsBridge.getHost();
+ Gateway gateway = gateways.get(host);
+ if (gateway == null) {
+ try {
+ addGateway(new Gateway(modelsEIPService, secrets, modelsBridge, apiVersion));
+ } catch (ConfigParser.ConfigParseError | JSONException | IOException e) {
+ e.printStackTrace();
+ }
+ } else {
+ addGateway(gateway.addTransport(Transport.createTransportFrom(modelsBridge)));
+ }
+ }
+ }
+
+ if (BuildConfig.BUILD_TYPE.equals("debug")) {
+ handleGatewayPinning();
+ }
}
private void parseSimpleGatewayList(Provider provider) {
- try {
- JSONObject geoIpJson = provider.getGeoIpJson();
- JSONArray gatewaylist = geoIpJson.getJSONArray(GATEWAYS);
-
- for (int i = 0; i < gatewaylist.length(); i++) {
- try {
- String key = gatewaylist.getString(i);
- if (gateways.containsKey(key)) {
- presortedList.add(gateways.get(key));
- }
- } catch (JSONException e) {
- e.printStackTrace();
- }
- }
- } catch (NullPointerException | JSONException npe) {
- Log.d(TAG, "No valid geoip json found: " + npe.getLocalizedMessage());
- }
+ try {
+ JSONObject geoIpJson = provider.getGeoIpJson();
+ JSONArray gatewaylist = geoIpJson.getJSONArray(GATEWAYS);
+
+ for (int i = 0; i < gatewaylist.length(); i++) {
+ try {
+ String key = gatewaylist.getString(i);
+ if (gateways.containsKey(key)) {
+ presortedList.add(gateways.get(key));
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ } catch (NullPointerException | JSONException npe) {
+ Log.d(TAG, "No valid geoip json found: " + npe.getLocalizedMessage());
+ }
}
private boolean hasSortedGatewaysWithLoad(@Nullable Provider provider) {
- if (provider == null) {
- return false;
- }
- JSONObject geoIpJson = provider.getGeoIpJson();
- return geoIpJson.has(SORTED_GATEWAYS);
+ if (provider == null) {
+ return false;
+ }
+ JSONObject geoIpJson = provider.getGeoIpJson();
+ return geoIpJson.has(SORTED_GATEWAYS);
}
private void parseGatewaysWithLoad(Provider provider) {
@@ -503,30 +615,28 @@ public class GatewaysManager {
}
private void configureFromCurrentProvider() {
- Provider provider = ProviderObservable.getInstance().getCurrentProvider();
- parseDefaultGateways(provider);
- if (BuildConfig.BUILD_TYPE.equals("debug") && handleGatewayPinning()) {
- return;
- }
- if (hasSortedGatewaysWithLoad(provider)) {
- parseGatewaysWithLoad(provider);
- } else {
- parseSimpleGatewayList(provider);
- }
-
+ Provider provider = ProviderObservable.getInstance().getCurrentProvider();
+ if (provider == null) {
+ return;
+ }
+ if (provider.getApiVersion() < 5) {
+ parseGatewaysV3(provider);
+ } else {
+ parseGatewaysV5(provider);
+ }
}
private boolean handleGatewayPinning() {
- String host = PreferenceHelper.getPinnedGateway();
- if (host == null) {
- return false;
- }
- Gateway gateway = gateways.get(host);
- gateways.clear();
- if (gateway != null) {
- gateways.put(host, gateway);
- }
- return true;
+ String host = PreferenceHelper.getPinnedGateway();
+ if (host == null) {
+ return false;
+ }
+ Gateway gateway = gateways.get(host);
+ gateways.clear();
+ if (gateway != null) {
+ gateways.put(host, gateway);
+ }
+ return true;
}
}
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 4c8fa797..76e32349 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java
@@ -19,50 +19,50 @@ package se.leap.bitmaskclient.eip;
import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_HOP;
import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN;
-import static se.leap.bitmaskclient.base.models.Constants.CAPABILITIES;
-import static se.leap.bitmaskclient.base.models.Constants.IP_ADDRESS;
-import static se.leap.bitmaskclient.base.models.Constants.IP_ADDRESS6;
import static se.leap.bitmaskclient.base.models.Constants.KCP;
-import static se.leap.bitmaskclient.base.models.Constants.PORTS;
-import static se.leap.bitmaskclient.base.models.Constants.PROTOCOLS;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.base.models.Constants.QUIC;
import static se.leap.bitmaskclient.base.models.Constants.REMOTE;
import static se.leap.bitmaskclient.base.models.Constants.TCP;
-import static se.leap.bitmaskclient.base.models.Constants.TRANSPORT;
import static se.leap.bitmaskclient.base.models.Constants.UDP;
-import static se.leap.bitmaskclient.base.utils.BuildConfigHelper.useObfsVpn;
-import static se.leap.bitmaskclient.pluggableTransports.ShapeshifterClient.DISPATCHER_IP;
-import static se.leap.bitmaskclient.pluggableTransports.ShapeshifterClient.DISPATCHER_PORT;
-
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.allowExperimentalTransports;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getExcludedApps;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getObfuscationPinningCert;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getObfuscationPinningGatewayLocation;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getObfuscationPinningIP;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getObfuscationPinningPort;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getObfuscationPinningProtocol;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getPreferUDP;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useObfuscationPinning;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-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.HashSet;
import java.util.Iterator;
import java.util.Set;
+import java.util.Vector;
import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.ConfigParser;
import de.blinkt.openvpn.core.VpnStatus;
-import de.blinkt.openvpn.core.connection.Connection;
import de.blinkt.openvpn.core.connection.Connection.TransportType;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.base.models.Transport;
import se.leap.bitmaskclient.base.utils.ConfigHelper;
-import se.leap.bitmaskclient.pluggableTransports.HoppingObfsVpnClient;
-import se.leap.bitmaskclient.pluggableTransports.Obfs4Options;
+import se.leap.bitmaskclient.pluggableTransports.ObfsvpnClient;
+import se.leap.bitmaskclient.pluggableTransports.models.Obfs4Options;
public class VpnConfigGenerator {
private final JSONObject generalConfiguration;
- private final JSONObject gateway;
private final JSONObject secrets;
- HashMap<TransportType, Transport> transports = new HashMap<>();
+ Vector<Transport> transports = new Vector<>();
private final int apiVersion;
private final boolean preferUDP;
private final boolean experimentalTransports;
@@ -70,8 +70,9 @@ public class VpnConfigGenerator {
private final String obfuscationPinningIP;
private final String obfuscationPinningPort;
private final String obfuscationPinningCert;
- private final boolean obfuscationPinningKCP;
+ private final String obfuscationPinningTransportProtocol;
private final String remoteGatewayIP;
+ private final String remoteGatewayIPv6;
private final String profileName;
private final Set<String> excludedApps;
@@ -84,19 +85,42 @@ public class VpnConfigGenerator {
boolean preferUDP;
boolean experimentalTransports;
String remoteGatewayIP = "";
+ String remoteGatewayIPv6 = "";
String profileName = "";
Set<String> excludedApps = null;
boolean useObfuscationPinning;
- boolean obfuscationProxyKCP;
+ String obfuscationProxyTransportProtocol = "";
String obfuscationProxyIP = "";
String obfuscationProxyPort = "";
String obfuscationProxyCert = "";
+ Vector<Transport> transports = new Vector<>();
+
+ public static VpnConfigGenerator.Configuration createProfileConfig(Vector<Transport> transports, int apiVersion, String remoteIpAddress, String remoteIpAddressV6, String locationName) {
+ VpnConfigGenerator.Configuration config = new VpnConfigGenerator.Configuration();
+ config.apiVersion = apiVersion;
+ config.preferUDP = getPreferUDP();
+ config.experimentalTransports = allowExperimentalTransports();
+ config.excludedApps = getExcludedApps();
+
+ config.remoteGatewayIP = config.useObfuscationPinning ? getObfuscationPinningIP() : remoteIpAddress;
+ config.remoteGatewayIPv6 = config.useObfuscationPinning ? null : remoteIpAddressV6;
+ config.useObfuscationPinning = useObfuscationPinning();
+ config.profileName = config.useObfuscationPinning ? getObfuscationPinningGatewayLocation() : locationName;
+ if (config.useObfuscationPinning) {
+ config.obfuscationProxyIP = getObfuscationPinningIP();
+ config.obfuscationProxyPort = getObfuscationPinningPort();
+ config.obfuscationProxyCert = getObfuscationPinningCert();
+ config.obfuscationProxyTransportProtocol = getObfuscationPinningProtocol();
+ }
+ config.transports = transports;
+ return config;
+ }
}
- public VpnConfigGenerator(JSONObject generalConfiguration, JSONObject secrets, JSONObject gateway, Configuration config) throws ConfigParser.ConfigParseError {
+
+ public VpnConfigGenerator(JSONObject generalConfiguration, JSONObject secrets, Configuration config) {
this.generalConfiguration = generalConfiguration;
- this.gateway = gateway;
this.secrets = secrets;
this.apiVersion = config.apiVersion;
this.preferUDP = config.preferUDP;
@@ -105,70 +129,45 @@ public class VpnConfigGenerator {
this.obfuscationPinningIP = config.obfuscationProxyIP;
this.obfuscationPinningPort = config.obfuscationProxyPort;
this.obfuscationPinningCert = config.obfuscationProxyCert;
- this.obfuscationPinningKCP = config.obfuscationProxyKCP;
+ this.obfuscationPinningTransportProtocol = config.obfuscationProxyTransportProtocol;
this.remoteGatewayIP = config.remoteGatewayIP;
+ this.remoteGatewayIPv6 = config.remoteGatewayIPv6;
+ this.transports = config.transports;
this.profileName = config.profileName;
this.excludedApps = config.excludedApps;
- checkCapabilities();
- }
-
- 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++) {
- Transport transport = Transport.fromJson(supportedTransports.getJSONObject(i));
- transports.put(transport.getTransportType(), transport);
- }
- }
- } catch (Exception e) {
- throw new ConfigParser.ConfigParseError("Api version ("+ apiVersion +") did not match required JSON fields");
- }
}
- public HashMap<TransportType, VpnProfile> generateVpnProfiles() throws
+ public Vector<VpnProfile> generateVpnProfiles() throws
ConfigParser.ConfigParseError,
NumberFormatException {
- HashMap<Connection.TransportType, VpnProfile> profiles = new HashMap<>();
- if (supportsOpenvpn()) {
+ Vector<VpnProfile> profiles = new Vector<>();
+
+ for (Transport transport : transports){
+ if (transport.getTransportType().isPluggableTransport()) {
+ Transport.Options transportOptions = transport.getOptions();
+ if (!experimentalTransports && transportOptions != null && transportOptions.isExperimental()) {
+ continue;
+ }
+ } else if (transport.getTransportType() == OPENVPN && useObfuscationPinning) {
+ continue;
+ }
try {
- profiles.put(OPENVPN, createProfile(OPENVPN));
+ profiles.add(createProfile(transport));
} catch (ConfigParser.ConfigParseError | NumberFormatException | JSONException | IOException e) {
e.printStackTrace();
}
}
- if (apiVersion >= 3) {
- for (TransportType transportType : transports.keySet()) {
- Transport transport = transports.get(transportType);
- if (transportType.isPluggableTransport()) {
- Transport.Options transportOptions = transport.getOptions();
- if (!experimentalTransports && transportOptions != null && transportOptions.isExperimental()) {
- continue;
- }
- try {
- profiles.put(transportType, createProfile(transportType));
- } catch (ConfigParser.ConfigParseError | NumberFormatException | JSONException | IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
+
if (profiles.isEmpty()) {
throw new ConfigParser.ConfigParseError("No supported transports detected.");
}
return profiles;
}
- private boolean supportsOpenvpn() {
- return !useObfuscationPinning &&
- ((apiVersion >= 3 && transports.containsKey(OPENVPN)) ||
- (apiVersion < 3 && !gatewayConfiguration(OPENVPN).isEmpty()));
- }
-
- private String getConfigurationString(TransportType transportType) {
+ private String getConfigurationString(Transport transport) {
return generalConfiguration()
+ newLine
- + gatewayConfiguration(transportType)
+ + gatewayConfiguration(transport)
+ newLine
+ androidCustomizations()
+ newLine
@@ -176,12 +175,13 @@ public class VpnConfigGenerator {
}
@VisibleForTesting
- protected VpnProfile createProfile(TransportType transportType) throws IOException, ConfigParser.ConfigParseError, JSONException {
- String configuration = getConfigurationString(transportType);
+ protected VpnProfile createProfile(Transport transport) throws IOException, ConfigParser.ConfigParseError, JSONException {
+ TransportType transportType = transport.getTransportType();
+ String configuration = getConfigurationString(transport);
ConfigParser icsOpenvpnConfigParser = new ConfigParser();
icsOpenvpnConfigParser.parseConfig(new StringReader(configuration));
if (transportType == OBFS4 || transportType == OBFS4_HOP) {
- icsOpenvpnConfigParser.setObfs4Options(getObfs4Options(transportType));
+ icsOpenvpnConfigParser.setObfs4Options(getObfs4Options(transport));
}
VpnProfile profile = icsOpenvpnConfigParser.convertProfile(transportType);
@@ -193,17 +193,14 @@ public class VpnConfigGenerator {
return profile;
}
- private Obfs4Options getObfs4Options(TransportType transportType) throws JSONException {
- String ip = gateway.getString(IP_ADDRESS);
- Transport transport;
+ private Obfs4Options getObfs4Options(Transport transport) throws JSONException, NullPointerException {
+ String ip = remoteGatewayIP;
if (useObfuscationPinning) {
transport = new Transport(OBFS4.toString(),
- new String[]{obfuscationPinningKCP ? KCP : TCP},
+ new String[]{obfuscationPinningTransportProtocol},
new String[]{obfuscationPinningPort},
obfuscationPinningCert);
ip = obfuscationPinningIP;
- } else {
- transport = transports.get(transportType);
}
return new Obfs4Options(ip, transport);
}
@@ -211,9 +208,9 @@ public class VpnConfigGenerator {
private String generalConfiguration() {
String commonOptions = "";
try {
- Iterator keys = generalConfiguration.keys();
+ Iterator<String> keys = generalConfiguration.keys();
while (keys.hasNext()) {
- String key = keys.next().toString();
+ String key = keys.next();
commonOptions += key + " ";
for (String word : String.valueOf(generalConfiguration.get(key)).split(" "))
@@ -231,32 +228,21 @@ public class VpnConfigGenerator {
return commonOptions;
}
- private String gatewayConfiguration(TransportType transportType) {
+ private String gatewayConfiguration(@NonNull Transport transport) {
String configs = "";
StringBuilder stringBuilder = new StringBuilder();
try {
- String ipAddress = null;
- JSONObject capabilities = gateway.getJSONObject(CAPABILITIES);
switch (apiVersion) {
- default:
- case 1:
- case 2:
- ipAddress = gateway.getString(IP_ADDRESS);
- gatewayConfigApiv1(stringBuilder, ipAddress, capabilities);
- break;
- case 3:
- case 4:
- ipAddress = gateway.optString(IP_ADDRESS);
- String ipAddress6 = gateway.optString(IP_ADDRESS6);
- String[] ipAddresses = ipAddress6.isEmpty() ?
- new String[]{ipAddress} :
- new String[]{ipAddress6, ipAddress};
-
- gatewayConfigMinApiv3(transportType, stringBuilder, ipAddresses);
- break;
+ case 1, 2 -> gatewayConfigApiv1(transport, stringBuilder, remoteGatewayIP);
+ case 3, 4, 5 -> {
+ String[] ipAddresses = (remoteGatewayIPv6 == null || remoteGatewayIPv6.isEmpty()) ?
+ new String[]{remoteGatewayIP} :
+ new String[]{remoteGatewayIPv6, remoteGatewayIP};
+ gatewayConfigMinApiv3(transport, stringBuilder, ipAddresses);
+ }
}
- } catch (JSONException e) {
+ } catch (JSONException | NullPointerException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
@@ -269,31 +255,40 @@ public class VpnConfigGenerator {
return configs;
}
- private void gatewayConfigMinApiv3(TransportType transportType, StringBuilder stringBuilder, String[] ipAddresses) throws JSONException {
- if (transportType.isPluggableTransport()) {
- ptGatewayConfigMinApiv3(stringBuilder, ipAddresses, transports.get(transportType));
+ private void gatewayConfigMinApiv3(Transport transport, StringBuilder stringBuilder, String[] ipAddresses) throws JSONException {
+ if (transport.getTransportType().isPluggableTransport()) {
+ ptGatewayConfigMinApiv3(stringBuilder, ipAddresses, transport);
} else {
- ovpnGatewayConfigMinApi3(stringBuilder, ipAddresses, transports.get(OPENVPN));
+ ovpnGatewayConfigMinApi3(stringBuilder, ipAddresses, transport);
+ }
+ }
+
+ private @Nullable Transport getTransport(TransportType transportType) {
+ for (Transport transport : transports) {
+ if (transport.getTransportType() == transportType) {
+ return transport;
+ }
}
+ return null;
}
- private void gatewayConfigApiv1(StringBuilder stringBuilder, String ipAddress, JSONObject capabilities) throws JSONException {
- int port;
- String protocol;
- JSONArray ports = capabilities.getJSONArray(PORTS);
- JSONArray protocols = capabilities.getJSONArray(PROTOCOLS);
- for (int i = 0; i < ports.length(); i++) {
- port = ports.getInt(i);
- for (int j = 0; j < protocols.length(); j++) {
- protocol = protocols.optString(j);
+ private void gatewayConfigApiv1(Transport transport, StringBuilder stringBuilder, String ipAddress) throws JSONException {
+ if (transport == null || transport.getProtocols() == null || transport.getPorts() == null) {
+ VpnStatus.logError("Misconfigured provider: missing details for transport openvpn on gateway " + ipAddress);
+ return;
+ }
+ String[] ports = transport.getPorts();
+ String[] protocols = transport.getProtocols();
+ for (String port : ports) {
+ for (String protocol : protocols) {
String newRemote = REMOTE + " " + ipAddress + " " + port + " " + protocol + newLine;
stringBuilder.append(newRemote);
}
}
}
- private void ovpnGatewayConfigMinApi3(StringBuilder stringBuilder, String[] ipAddresses, Transport transport) {
- if (transport.getProtocols() == null || transport.getPorts() == null) {
+ private void ovpnGatewayConfigMinApi3(StringBuilder stringBuilder, String[] ipAddresses, @Nullable Transport transport) {
+ if (transport == null || transport.getProtocols() == null || transport.getPorts() == null) {
VpnStatus.logError("Misconfigured provider: missing details for transport openvpn on gateway " + ipAddresses[0]);
return;
}
@@ -332,7 +327,7 @@ public class VpnConfigGenerator {
return TCP.equals(protocol) || UDP.equals(protocol);
case OBFS4_HOP:
case OBFS4:
- return TCP.equals(protocol) || KCP.equals(protocol);
+ return TCP.equals(protocol) || KCP.equals(protocol) || QUIC.equals(protocol);
}
return false;
}
@@ -384,28 +379,12 @@ public class VpnConfigGenerator {
}
stringBuilder.append(getRouteString(ipAddress, transport));
- stringBuilder.append(getRemoteString(ipAddress, transport));
- stringBuilder.append(getExtraOptions(transport));
- }
-
- public String getRemoteString(String ipAddress, Transport transport) {
- if (useObfsVpn()) {
- if (useObfuscationPinning) {
- return REMOTE + " " + obfuscationPinningIP + " " + obfuscationPinningPort + " tcp" + newLine;
- }
- switch (transport.getTransportType()) {
- case OBFS4:
- return REMOTE + " " + ipAddress + " " + transport.getPorts()[0] + " tcp" + newLine;
- case OBFS4_HOP:
- return REMOTE + " " + HoppingObfsVpnClient.IP + " " + HoppingObfsVpnClient.PORT + " udp" + newLine;
- default:
- VpnStatus.logError("Unexpected pluggable transport type " + transport.getType() + " for gateway " + ipAddress);
- return "";
- }
- }
- return REMOTE + " " + DISPATCHER_IP + " " + DISPATCHER_PORT + " tcp" + newLine;
+ String transparentProxyRemote = REMOTE + " " + ObfsvpnClient.IP + " " + ObfsvpnClient.DEFAULT_PORT + " udp" + newLine;
+ stringBuilder.append(transparentProxyRemote);
}
+ // TODO: figure out if any of these configs still make sense (
+ @Deprecated
public String getExtraOptions(Transport transport) {
if (transport.getTransportType() == OBFS4_HOP) {
return "replay-window 65535" + newLine +
@@ -437,14 +416,14 @@ public class VpnConfigGenerator {
return "";
}
- // While openvpn in TCP mode is required for obfs4, openvpn in UDP mode is required for obfs4-hop
+ // With obfsvpn 1.0.0 openvpn is always required to run in UDP to work with any obfs4 based pluggable transport.
private boolean openvpnModeSupportsPt(Transport transport, String ipAddress) {
if (useObfuscationPinning) {
// we don't know if the manually pinned bridge points to a openvpn gateway with the right
// configuration, so we assume yes
return true;
}
- Transport openvpnTransport = transports.get(OPENVPN);
+ Transport openvpnTransport = getTransport(OPENVPN);
if (openvpnTransport == null) {
// the bridge seems to be to be decoupled from the gateway, we can't say if the openvpn gateway
// will support this PT and hope the admins configured the gateway correctly
@@ -457,14 +436,14 @@ public class VpnConfigGenerator {
return false;
}
- String requiredProtocol = transport.getTransportType() == OBFS4_HOP ? UDP : TCP;
+ String requiredProtocol = UDP;
for (String protocol : protocols) {
if (protocol.equals(requiredProtocol)) {
return true;
}
}
- VpnStatus.logError("Misconfigured provider: " + transport.getTransportType().toString() + " currently only allows openvpn in " + requiredProtocol + " mode! Skipping config for ip " + ipAddress);
+ VpnStatus.logError("Provider - client incompatibility: obfuscation protocol " + transport.getTransportType().toString() + " currently only allows OpenVPN in " + requiredProtocol + " mode! Skipping config for ip " + ipAddress);
return false;
}
@@ -473,6 +452,8 @@ public class VpnConfigGenerator {
for (String protocol : ptProtocols) {
if (isAllowedProtocol(transport.getTransportType(), protocol)) {
return true;
+ } else {
+ VpnStatus.logError("Provider - client incompatibility: " + protocol + " is not an allowed transport layer protocol for " + transport.getType());
}
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/HoppingObfsVpnClient.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/HoppingObfsVpnClient.java
deleted file mode 100644
index 751208ba..00000000
--- a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/HoppingObfsVpnClient.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package se.leap.bitmaskclient.pluggableTransports;
-
-import client.Client;
-import client.HopClient;
-import de.blinkt.openvpn.core.VpnStatus;
-import se.leap.bitmaskclient.base.models.Constants;
-
-public class HoppingObfsVpnClient implements PtClientInterface {
-
- public static final int PORT = 8080;
- public static final String IP = "127.0.0.1";
-
- public final HopClient client;
-
- public HoppingObfsVpnClient(Obfs4Options options) throws IllegalStateException {
-
- //FIXME: use a different strategy here
- //Basically we would want to track if the more performant transport protocol (KCP?/TCP?) usage was successful
- //if so, we stick to it, otherwise we flip the flag
- boolean kcp = Constants.KCP.equals(options.transport.getProtocols()[0]);
-
- HoppingConfig hoppingConfig = new HoppingConfig(kcp,IP+":"+PORT, options, 10, 10);
- try {
- client = Client.newFFIHopClient(hoppingConfig.toString());
- } catch (Exception e) {
- throw new IllegalStateException(e);
- }
- }
-
- @Override
- public int start() {
- try {
- client.setEventLogger(this);
- return client.start() ? PORT : 0;
- } catch (Exception e) {
- e.printStackTrace();
- return 0;
- }
- }
-
- @Override
- public void stop() {
- try {
- client.stop();
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- client.setEventLogger(null);
- }
- }
-
- @Override
- public boolean isStarted() {
- return client.isStarted();
- }
-
- @Override
- public void error(String s) {
- VpnStatus.logError("[hopping-obfs4] " + s);
- }
-
- @Override
- public void log(String state, String message) {
- VpnStatus.logDebug("[hopping-obfs4] " + state + ": " + message);
- }
-}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsVpnClient.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsVpnClient.java
deleted file mode 100644
index 685349ed..00000000
--- a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsVpnClient.java
+++ /dev/null
@@ -1,144 +0,0 @@
-package se.leap.bitmaskclient.pluggableTransports;
-
-import static se.leap.bitmaskclient.base.models.Constants.KCP;
-
-import android.util.Log;
-
-import java.beans.PropertyChangeEvent;
-import java.beans.PropertyChangeListener;
-import java.util.Observable;
-import java.util.Observer;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import client.Client;
-import de.blinkt.openvpn.core.ConnectionStatus;
-import de.blinkt.openvpn.core.VpnStatus;
-import se.leap.bitmaskclient.eip.EipStatus;
-
-public class ObfsVpnClient implements PropertyChangeListener, PtClientInterface {
-
- public static final AtomicInteger SOCKS_PORT = new AtomicInteger(4430);
- public static final String SOCKS_IP = "127.0.0.1";
- private static final String ERR_BIND = "bind: address already in use";
-
- private static final String TAG = ObfsVpnClient.class.getSimpleName();
- private volatile boolean noNetwork;
- private final AtomicBoolean pendingNetworkErrorHandling = new AtomicBoolean(false);
- private final AtomicInteger reconnectRetry = new AtomicInteger(0);
- private static final int MAX_RETRY = 5;
-
- private final client.Client_ obfsVpnClient;
- private final Object LOCK = new Object();
-
- public ObfsVpnClient(Obfs4Options options) throws IllegalStateException{
- //FIXME: use a different strategy here
- //Basically we would want to track if the more performant transport protocol (KCP?/TCP?) usage was successful
- //if so, we stick to it, otherwise we flip the flag
- boolean kcp = KCP.equals(options.transport.getProtocols()[0]);
-
- if (options.transport.getOptions().getCert() == null) {
- throw new IllegalStateException("No cert found to establish a obfs4 connection");
- }
-
- obfsVpnClient = Client.newClient(kcp, SOCKS_IP+":"+SOCKS_PORT.get(), options.transport.getOptions().getCert());
- }
-
- /**
- * starts the client
- * @return the port ObfsVpn is running on
- */
- public int start() {
- synchronized (LOCK) {
- obfsVpnClient.setEventLogger(this);
- Log.d(TAG, "aquired LOCK");
- new Thread(this::startSync).start();
- waitUntilStarted();
- Log.d(TAG, "returning LOCK after " + (reconnectRetry.get() + 1) * 200 +" ms");
- }
- return SOCKS_PORT.get();
- }
-
- // We're waiting here until the obfsvpn client has found a unbound port and started
- private void waitUntilStarted() {
- int count = -1;
- try {
- while (count < reconnectRetry.get() && reconnectRetry.get() < MAX_RETRY) {
- Thread.sleep(200);
- count++;
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- private void startSync() {
- try {
- obfsVpnClient.start();
- } catch (Exception e) {
- Log.e(TAG, "[obfsvpn] exception: " + e.getLocalizedMessage());
- VpnStatus.logError("[obfsvpn] " + e.getLocalizedMessage());
- if (e.getLocalizedMessage() != null && e.getLocalizedMessage().contains(ERR_BIND) && reconnectRetry.get() < MAX_RETRY) {
- reconnectRetry.addAndGet(1);
- SOCKS_PORT.addAndGet(1);
- obfsVpnClient.setSocksAddr(SOCKS_IP+":"+SOCKS_PORT.get());
- Log.d(TAG, "[obfsvpn] reconnecting on different port... " + SOCKS_PORT.get());
- VpnStatus.logDebug("[obfsvpn] reconnecting on different port... " + SOCKS_PORT.get());
- startSync();
- } else if (noNetwork) {
- pendingNetworkErrorHandling.set(true);
- }
- }
- }
-
- public void stop() {
- synchronized (LOCK) {
- Log.d(TAG, "stopping obfsVpnClient...");
- try {
- obfsVpnClient.stop();
- reconnectRetry.set(0);
- SOCKS_PORT.set(4430);
- Thread.sleep(100);
- } catch (Exception e) {
- e.printStackTrace();
- VpnStatus.logError("[obfsvpn] " + e.getLocalizedMessage());
- } finally {
- obfsVpnClient.setEventLogger(null);
- }
- pendingNetworkErrorHandling.set(false);
- Log.d(TAG, "stopping obfsVpnClient releasing LOCK ...");
- }
- }
-
- public boolean isStarted() {
- return obfsVpnClient.isStarted();
- }
-
- // TODO: register observer!
- @Override
- public void propertyChange(PropertyChangeEvent evt) {
- if (EipStatus.PROPERTY_CHANGE.equals(evt.getPropertyName())) {
- EipStatus status = (EipStatus) evt.getNewValue();
- if (status.getLevel() == ConnectionStatus.LEVEL_NONETWORK) {
- noNetwork = true;
- } else {
- noNetwork = false;
- if (pendingNetworkErrorHandling.getAndSet(false)) {
- stop();
- start();
- }
- }
- }
- }
-
- @Override
- public void error(String s) {
- VpnStatus.logError("[obfsvpn] " + s);
- }
-
- @Override
- public void log(String state, String message) {
- VpnStatus.logDebug("[obfsvpn] " + state + " " + message);
- }
-
-}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsvpnClient.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsvpnClient.java
new file mode 100644
index 00000000..2e216015
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsvpnClient.java
@@ -0,0 +1,180 @@
+package se.leap.bitmaskclient.pluggableTransports;
+
+import static se.leap.bitmaskclient.base.models.Constants.KCP;
+import static se.leap.bitmaskclient.base.models.Constants.QUIC;
+
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import client.Client;
+import client.Client_;
+import client.EventLogger;
+import de.blinkt.openvpn.core.VpnStatus;
+import de.blinkt.openvpn.core.connection.Connection;
+import se.leap.bitmaskclient.pluggableTransports.models.HoppingConfig;
+import se.leap.bitmaskclient.pluggableTransports.models.KcpConfig;
+import se.leap.bitmaskclient.pluggableTransports.models.Obfs4Options;
+import se.leap.bitmaskclient.pluggableTransports.models.ObfsvpnConfig;
+import se.leap.bitmaskclient.pluggableTransports.models.QuicConfig;
+
+public class ObfsvpnClient implements EventLogger {
+
+ public static final int DEFAULT_PORT = 8080;
+ public static final String IP = "127.0.0.1";
+ private static final String ERROR_BIND = "bind: address already in use";
+ private static final String STATE_RUNNING = "RUNNING";
+ private final Object LOCK = new Object();
+ private final AtomicInteger currentPort = new AtomicInteger(DEFAULT_PORT);
+ private CountDownLatch startCallback = null;
+
+ private static final String TAG = ObfsvpnClient.class.getSimpleName();
+
+ public final Client_ client;
+
+ public ObfsvpnClient(Obfs4Options options) throws IllegalStateException {
+ // each obfuscation transport has only 1 protocol
+ String protocol = options.transport.getProtocols()[0];
+ boolean kcpEnabled = KCP.equals(protocol);
+ boolean quicEnabled = QUIC.equals(protocol);
+ boolean hoppingEnabled = options.transport.getTransportType() == Connection.TransportType.OBFS4_HOP;
+ if (!hoppingEnabled && (options.transport.getPorts() == null || options.transport.getPorts().length == 0)) {
+ throw new IllegalStateException("obf4 based transport has no bridge ports configured");
+ }
+ KcpConfig kcpConfig = new KcpConfig(kcpEnabled);
+ QuicConfig quicConfig = new QuicConfig(quicEnabled);
+ HoppingConfig hoppingConfig = new HoppingConfig(hoppingEnabled,IP+":"+ DEFAULT_PORT, options);
+ ObfsvpnConfig obfsvpnConfig = new ObfsvpnConfig(IP+":"+ DEFAULT_PORT, hoppingConfig, kcpConfig, quicConfig, options.bridgeIP, options.transport.getPorts()[0], options.transport.getOptions().getCert() );
+ try {
+ Log.d(TAG, "create new obfsvpn client: " + obfsvpnConfig);
+ client = Client.newFFIClient(obfsvpnConfig.toString());
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public void start() throws RuntimeException {
+ synchronized (LOCK) {
+ client.setEventLogger(this);
+
+ // this CountDownLatch stops blocking if:
+ // a) obfsvpn changed its state to RUNNING
+ // b) an unrecoverable error happened
+ final CountDownLatch callback = new CountDownLatch(1);
+ this.startCallback = callback;
+ AtomicReference<Exception> err = new AtomicReference<>();
+ new Thread(() -> {
+ try {
+ start(0);
+ } catch (RuntimeException e) {
+ // save exception and stop blocking
+ e.printStackTrace();
+ err.set(e);
+ callback.countDown();
+ }
+ }).start();
+
+ try {
+ boolean completedBeforeTimeout = callback.await(35, TimeUnit.SECONDS);
+ Exception startException = err.get();
+ this.startCallback = null;
+ if (!completedBeforeTimeout) {
+ client.setEventLogger(null);
+ throw new RuntimeException("failed to start obfsvpn: timeout error");
+ } else if (startException != null) {
+ client.setEventLogger(null);
+ throw new RuntimeException("failed to start obfsvpn: ", startException);
+ }
+ } catch (InterruptedException e) {
+ this.startCallback = null;
+ client.setEventLogger(null);
+ throw new RuntimeException("failed to start obfsvpn: ", e);
+ }
+ }
+ }
+
+ private void start(int portOffset) throws RuntimeException {
+ currentPort.set(DEFAULT_PORT + portOffset);
+ Log.d(TAG, "listen to 127.0.0.1:"+ (currentPort.get()));
+ final CountDownLatch errOnStartCDL = new CountDownLatch(1);
+ AtomicReference<Exception> err = new AtomicReference<>();
+ new Thread(() -> {
+ try {
+ client.setProxyAddr(IP + ":" + (DEFAULT_PORT+portOffset));
+ client.start();
+ } catch (Exception e) {
+ err.set(e);
+ errOnStartCDL.countDown();
+ }
+ }).start();
+
+ try {
+ // wait for 250 ms, in case there is an immediate error due to misconfiguration
+ // or bound ports the CountDownLatch is set to 0 and thus the return value of await is true
+ boolean receivedErr = errOnStartCDL.await(250, TimeUnit.MILLISECONDS);
+ if (receivedErr) {
+ Exception e = err.get();
+ // attempt to restart the client with a different local proxy port in case
+ // there's a port binding error
+ if (e != null &&
+ e.getMessage() != null &&
+ e.getMessage().contains(ERROR_BIND) &&
+ portOffset < 10) {
+ start(portOffset + 1);
+ return;
+ } else {
+ resetAndThrow(new RuntimeException("Failed to start obfsvpn: " + e));
+ }
+ }
+ } catch (InterruptedException e) {
+ resetAndThrow(new RuntimeException(e));
+ }
+ }
+
+ private void resetAndThrow(RuntimeException e) throws RuntimeException{
+ startCallback.countDown();
+ startCallback = null;
+ client.setEventLogger(null);
+ throw e;
+ }
+
+ public boolean stop() {
+ synchronized (LOCK) {
+ try {
+ client.stop();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ } finally {
+ client.setEventLogger(null);
+ }
+ return true;
+ }
+ }
+
+ public int getPort() {
+ return currentPort.get();
+ }
+
+ public boolean isStarted() {
+ return client.isStarted();
+ }
+
+ @Override
+ public void error(String s) {
+ VpnStatus.logError("[obfsvpn-client] error: " + s);
+ }
+
+ @Override
+ public void log(String state, String message) {
+ VpnStatus.logDebug("[obfsvpn-client] " + state + ": " + message);
+ CountDownLatch startCallback = this.startCallback;
+ if (startCallback != null && STATE_RUNNING.equals(state)) {
+ startCallback.countDown();
+ this.startCallback = null;
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/PtClientBuilder.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/PtClientBuilder.java
deleted file mode 100644
index 945e3d7a..00000000
--- a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/PtClientBuilder.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package se.leap.bitmaskclient.pluggableTransports;
-
-import de.blinkt.openvpn.core.connection.Connection;
-import de.blinkt.openvpn.core.connection.Obfs4Connection;
-import de.blinkt.openvpn.core.connection.Obfs4HopConnection;
-
-public class PtClientBuilder {
- public static PtClientInterface getPtClient(Connection connection) throws IllegalStateException {
- switch (connection.getTransportType()) {
- case OBFS4:
- return new ObfsVpnClient(((Obfs4Connection) connection).getObfs4Options());
- case OBFS4_HOP:
- return new HoppingObfsVpnClient(((Obfs4HopConnection) connection).getObfs4Options());
- default:
- throw new IllegalStateException("Unexpected pluggable transport " + connection.getTransportType());
- }
- }
-}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/PtClientInterface.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/PtClientInterface.java
deleted file mode 100644
index 28d19a97..00000000
--- a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/PtClientInterface.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package se.leap.bitmaskclient.pluggableTransports;
-
-import client.EventLogger;
-
-public interface PtClientInterface extends EventLogger {
- int start();
- void stop();
- boolean isStarted();
-}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ShapeshifterClient.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ShapeshifterClient.java
deleted file mode 100644
index e57401f8..00000000
--- a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ShapeshifterClient.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/**
- * Copyright (c) 2020 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.os.Handler;
-import android.os.Looper;
-import android.util.Log;
-
-import java.beans.PropertyChangeEvent;
-import java.beans.PropertyChangeListener;
-
-import de.blinkt.openvpn.core.ConnectionStatus;
-import de.blinkt.openvpn.core.VpnStatus;
-import se.leap.bitmaskclient.eip.EipStatus;
-
-public class ShapeshifterClient implements PropertyChangeListener {
-
- public static final String DISPATCHER_PORT = "4430";
- public static final String DISPATCHER_IP = "127.0.0.1";
- private static final int MAX_RETRY = 5;
- private static final int RETRY_TIME = 4000;
- private static final String TAG = ShapeshifterClient.class.getSimpleName();
-
- private final shapeshifter.Shapeshifter_ shapeShifter;
- private boolean isErrorHandling;
- private boolean noNetwork;
- private int retry = 0;
- private final Handler reconnectHandler;
-
- @Deprecated
- public class ShapeshifterLogger implements shapeshifter.Logger {
- @Override
- public void log(String s) {
- Log.e(TAG, "SHAPESHIFTER ERROR: " + s);
- VpnStatus.logError(s);
- isErrorHandling = true;
- close();
-
- if (retry < MAX_RETRY && !noNetwork) {
- retry++;
- reconnectHandler.postDelayed(ShapeshifterClient.this::reconnect, RETRY_TIME);
- } else {
- VpnStatus.logError(VpnStatus.ErrorType.SHAPESHIFTER);
- }
- }
- }
-
- public ShapeshifterClient(Obfs4Options options) {
- shapeShifter = new shapeshifter.Shapeshifter_();
- shapeShifter.setLogger(new ShapeshifterLogger());
- setup(options);
- Looper.prepare();
- reconnectHandler = new Handler();
- EipStatus.getInstance().addObserver(this);
- Log.d(TAG, "shapeshifter initialized with: \n" + shapeShifter.toString());
- }
-
- private void setup(Obfs4Options options) {
- shapeShifter.setSocksAddr(DISPATCHER_IP+":"+DISPATCHER_PORT);
- shapeShifter.setTarget(options.gatewayIP +":"+options.transport.getPorts()[0]);
- shapeShifter.setCert(options.transport.getOptions().getCert());
- }
-
- public void setOptions(Obfs4Options options) {
- setup(options);
- }
-
- public void start() {
- try {
- shapeShifter.open();
- } catch (Exception e) {
- e.printStackTrace();
- Log.e(TAG, "SHAPESHIFTER ERROR: " + e.getLocalizedMessage());
- VpnStatus.logError(VpnStatus.ErrorType.SHAPESHIFTER);
- VpnStatus.logError(e.getLocalizedMessage());
- }
- }
-
- private void close() {
- try {
- shapeShifter.close();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- private void reconnect() {
- try {
- shapeShifter.open();
- retry = 0;
- isErrorHandling = false;
- } catch (Exception e) {
- e.printStackTrace();
- Log.e(TAG, "SHAPESHIFTER RECONNECTION ERROR: " + e.getLocalizedMessage());
- VpnStatus.logError("Unable to reconnect shapeshifter: " + e.getLocalizedMessage());
- }
- }
-
- public boolean stop() {
- try {
- shapeShifter.close();
- return true;
- } catch (Exception e) {
- e.printStackTrace();
- VpnStatus.logError(e.getLocalizedMessage());
- }
- EipStatus.getInstance().deleteObserver(this);
- return false;
- }
-
-
- @Override
- public void propertyChange(PropertyChangeEvent evt) {
- if (EipStatus.PROPERTY_CHANGE.equals(evt.getPropertyName())) {
- EipStatus status = (EipStatus) evt.getNewValue();
- if (status.getLevel() == ConnectionStatus.LEVEL_NONETWORK) {
- noNetwork = true;
- } else {
- noNetwork = false;
- if (isErrorHandling) {
- isErrorHandling = false;
- close();
- start();
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/HoppingConfig.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/HoppingConfig.java
index 3780b7dc..0dc2d508 100644
--- a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/HoppingConfig.java
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/HoppingConfig.java
@@ -1,4 +1,4 @@
-package se.leap.bitmaskclient.pluggableTransports;
+package se.leap.bitmaskclient.pluggableTransports.models;
import androidx.annotation.NonNull;
@@ -9,41 +9,56 @@ import com.google.gson.GsonBuilder;
import se.leap.bitmaskclient.base.models.Transport;
public class HoppingConfig {
- final boolean kcp;
+
+ /**
+ * Enabled bool `json:"enabled"`
+ * Remotes []string `json:"remotes"`
+ * Obfs4Certs []string `json:"obfs4_certs"`
+ * PortSeed int64 `json:"port_seed"`
+ * PortCount uint `json:"port_count"`
+ * MinHopPort uint `json:"min_hop_port"`
+ * MaxHopPort uint `json:"max_hop_port"`
+ * MinHopSeconds uint `json:"min_hop_seconds"`
+ * HopJitter uint `json:"hop_jitter"`
+ */
+
+ final boolean enabled;
final String proxyAddr;
final String[] remotes;
- final String[] certs;
+ final String[] obfs4Certs;
final int portSeed;
final int portCount;
final int minHopSeconds;
final int hopJitter;
+ final int minHopPort;
+ final int maxHopPort;
- public HoppingConfig(boolean kcp,
+ public HoppingConfig(boolean enabled,
String proxyAddr,
- Obfs4Options options,
- int minHopSeconds,
- int hopJitter) {
- this.kcp = kcp;
+ Obfs4Options options) {
+ this.enabled = enabled;
this.proxyAddr = proxyAddr;
Transport transport = options.transport;
Transport.Endpoint[] endpoints = transport.getOptions().getEndpoints();
if (endpoints == null) {
// only port hopping, we assume the gateway IP as hopping PT's IP
- this.remotes = new String[]{ options.gatewayIP };
- this.certs = new String[] { transport.getOptions().getCert() };
+ this.remotes = new String[]{ options.bridgeIP };
+ this.obfs4Certs = new String[] { transport.getOptions().getCert() };
} else {
// port+ip hopping
this.remotes = new String[endpoints.length];
- this.certs = new String[endpoints.length];
+ this.obfs4Certs = new String[endpoints.length];
for (int i = 0; i < remotes.length; i++) {
remotes[i] = endpoints[i].getIp();
- certs[i] = endpoints[i].getCert();
+ obfs4Certs[i] = endpoints[i].getCert();
}
}
this.portSeed = transport.getOptions().getPortSeed();
this.portCount = transport.getOptions().getPortCount();
- this.minHopSeconds = minHopSeconds;
- this.hopJitter = hopJitter;
+ this.minHopSeconds = transport.getOptions().getMinHopSeconds();
+ this.hopJitter = transport.getOptions().getHopJitter();
+ this.minHopPort = transport.getOptions().getMinHopPort();
+ this.maxHopPort = transport.getOptions().getMaxHopPort();
}
@NonNull
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/KcpConfig.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/KcpConfig.java
new file mode 100644
index 00000000..f056394d
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/KcpConfig.java
@@ -0,0 +1,37 @@
+package se.leap.bitmaskclient.pluggableTransports.models;
+
+import androidx.annotation.NonNull;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.annotations.SerializedName;
+
+public class KcpConfig {
+
+
+ final boolean enabled;
+ final int sendWindowSize = 65535;
+ final int receiveWindowSize = 65535;
+ final int readBuffer = 16 * 1024 * 1024;
+ final int writeBuffer = 16 * 1024 * 1024;
+ final boolean noDelay = true;
+ final boolean disableFlowControl = true;
+ final int interval = 10;
+ final int resend = 2;
+ @SerializedName("mtu")
+ final int MTU = 1400;
+
+ public KcpConfig(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ Gson gson = new GsonBuilder()
+ .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+ .create();
+ return gson.toJson(this);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Obfs4Options.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/Obfs4Options.java
index 0dd81eb8..1eec376a 100644
--- a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Obfs4Options.java
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/Obfs4Options.java
@@ -1,16 +1,16 @@
-package se.leap.bitmaskclient.pluggableTransports;
+package se.leap.bitmaskclient.pluggableTransports.models;
import java.io.Serializable;
import se.leap.bitmaskclient.base.models.Transport;
public class Obfs4Options implements Serializable {
- public String gatewayIP;
+ public String bridgeIP;
public Transport transport;
- public Obfs4Options(String gatewayIP,
+ public Obfs4Options(String bridgeIP,
Transport transport) {
- this.gatewayIP = gatewayIP;
+ this.bridgeIP = bridgeIP;
this.transport = transport;
}
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/ObfsvpnConfig.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/ObfsvpnConfig.java
new file mode 100644
index 00000000..cfcd6b6c
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/ObfsvpnConfig.java
@@ -0,0 +1,37 @@
+package se.leap.bitmaskclient.pluggableTransports.models;
+
+import androidx.annotation.NonNull;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+public class ObfsvpnConfig {
+
+ final String proxyAddr;
+ final HoppingConfig hoppingConfig;
+ final KcpConfig kcpConfig;
+ final QuicConfig quicConfig;
+ final String remoteIp;
+ final String remotePort;
+ final String obfs4Cert;
+
+ public ObfsvpnConfig(String proxyAddress, HoppingConfig hoppingConfig, KcpConfig kcpConfig, QuicConfig quicConfig, String remoteIP, String remotePort, String obfsv4Cert) {
+ this.proxyAddr = proxyAddress;
+ this.hoppingConfig = hoppingConfig;
+ this.kcpConfig = kcpConfig;
+ this.quicConfig = quicConfig;
+ this.remoteIp = remoteIP;
+ this.remotePort = remotePort;
+ this.obfs4Cert = obfsv4Cert;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ Gson gson = new GsonBuilder()
+ .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+ .create();
+ return gson.toJson(this);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/QuicConfig.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/QuicConfig.java
new file mode 100644
index 00000000..dd377dd7
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/QuicConfig.java
@@ -0,0 +1,24 @@
+package se.leap.bitmaskclient.pluggableTransports.models;
+
+import androidx.annotation.NonNull;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+public class QuicConfig {
+ final boolean enabled;
+
+ public QuicConfig(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ Gson gson = new GsonBuilder()
+ .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+ .create();
+ return gson.toJson(this);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/IProviderApiManager.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/IProviderApiManager.java
new file mode 100644
index 00000000..604dc060
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/IProviderApiManager.java
@@ -0,0 +1,11 @@
+package se.leap.bitmaskclient.providersetup;
+
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+import se.leap.bitmaskclient.base.models.Provider;
+
+public interface IProviderApiManager {
+ void handleAction(String action, Provider provider, Bundle parameters, ResultReceiver receiver);
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java
index 68699da2..63ae3731 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java
@@ -16,7 +16,6 @@
*/
package se.leap.bitmaskclient.providersetup;
-import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
@@ -31,11 +30,10 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.util.concurrent.TimeoutException;
import se.leap.bitmaskclient.base.models.Provider;
-import se.leap.bitmaskclient.providersetup.connectivity.OkHttpClientGenerator;
import se.leap.bitmaskclient.tor.TorServiceCommand;
/**
- * Implements HTTP api methods (encapsulated in {{@link ProviderApiManager}})
+ * Implements HTTP api methods (encapsulated in {{@link ProviderApiManagerV3}})
* used to manage communications with the provider server.
* <p/>
* It's an JobIntentService because it downloads data from the Internet, so it operates in the background.
@@ -83,12 +81,6 @@ public class ProviderAPI extends JobIntentService implements ProviderApiManagerB
DOWNLOAD_SERVICE_JSON = "ProviderAPI.DOWNLOAD_SERVICE_JSON";
final public static int
- SUCCESSFUL_LOGIN = 3,
- FAILED_LOGIN = 4,
- SUCCESSFUL_SIGNUP = 5,
- FAILED_SIGNUP = 6,
- SUCCESSFUL_LOGOUT = 7,
- LOGOUT_FAILED = 8,
CORRECTLY_DOWNLOADED_VPN_CERTIFICATE = 9,
INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE = 10,
PROVIDER_OK = 11,
@@ -105,17 +97,10 @@ public class ProviderAPI extends JobIntentService implements ProviderApiManagerB
ProviderApiManager providerApiManager;
- //TODO: refactor me, please!
- //used in insecure flavor only
- @SuppressLint("unused")
- public static boolean lastDangerOn() {
- return ProviderApiManager.lastDangerOn();
- }
-
@Override
public void onCreate() {
super.onCreate();
- providerApiManager = initApiManager();
+ providerApiManager = new ProviderApiManager(getResources(), this);
}
/**
@@ -155,6 +140,11 @@ public class ProviderAPI extends JobIntentService implements ProviderApiManagerB
}
@Override
+ public int getTorSocksProxyPort() {
+ return TorServiceCommand.getSocksProxyPort(this);
+ }
+
+ @Override
public boolean hasNetworkConnection() {
try {
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
@@ -184,10 +174,4 @@ public class ProviderAPI extends JobIntentService implements ProviderApiManagerB
pm.saveCustomProviders();
}
-
- private ProviderApiManager initApiManager() {
- OkHttpClientGenerator clientGenerator = new OkHttpClientGenerator(getResources());
- return new ProviderApiManager(getResources(), clientGenerator, this);
- }
-
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiEventSender.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiEventSender.java
new file mode 100644
index 00000000..1c8bd39b
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiEventSender.java
@@ -0,0 +1,180 @@
+package se.leap.bitmaskclient.providersetup;
+
+import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_PROVIDER_API_EVENT;
+import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_CODE;
+import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.getProviderFormattedString;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORID;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_GEOIP_JSON;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INITIAL_ACTION;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.PROVIDER_NOK;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.TOR_EXCEPTION;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.TOR_TIMEOUT;
+
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import de.blinkt.openvpn.core.VpnStatus;
+import se.leap.bitmaskclient.base.models.Provider;
+
+public class ProviderApiEventSender {
+
+ private final Resources resources;
+ private final ProviderApiManagerBase.ProviderApiServiceCallback serviceCallback;
+
+ public ProviderApiEventSender(Resources resources, ProviderApiManagerBase.ProviderApiServiceCallback callback) {
+ this.resources = resources;
+ this.serviceCallback = callback;
+ }
+
+ /**
+ * Interprets the error message as a JSON object and extract the "errors" keyword pair.
+ * If the error message is not a JSON object, then it is returned untouched.
+ *
+ * @param stringJsonErrorMessage
+ * @return final error message
+ */
+ protected String pickErrorMessage(String stringJsonErrorMessage) {
+ String errorMessage = "";
+ try {
+ JSONObject jsonErrorMessage = new JSONObject(stringJsonErrorMessage);
+ errorMessage = jsonErrorMessage.getString(ERRORS);
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ errorMessage = stringJsonErrorMessage;
+ } catch (NullPointerException e) {
+ //do nothing
+ }
+
+ return errorMessage;
+ }
+
+ protected Bundle setErrorResult(Bundle result, String stringJsonErrorMessage) {
+ String reasonToFail = pickErrorMessage(stringJsonErrorMessage);
+ VpnStatus.logWarning("[API] error: " + reasonToFail);
+ result.putString(ERRORS, reasonToFail);
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+ return result;
+ }
+
+ Bundle setErrorResultAction(Bundle result, String initialAction) {
+ JSONObject errorJson = new JSONObject();
+ addErrorMessageToJson(errorJson, null, null, initialAction);
+ VpnStatus.logWarning("[API] error: " + initialAction + " failed.");
+ result.putString(ERRORS, errorJson.toString());
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+ return result;
+ }
+
+ Bundle setErrorResult(Bundle result, int errorMessageId, String errorId) {
+ return setErrorResult(result, errorMessageId, errorId, null);
+ }
+
+ Bundle setErrorResult(Bundle result, int errorMessageId, String errorId, String initialAction) {
+ JSONObject errorJson = new JSONObject();
+ String errorMessage = getProviderFormattedString(resources, errorMessageId);
+ addErrorMessageToJson(errorJson, errorMessage, errorId, initialAction);
+ VpnStatus.logWarning("[API] error: " + errorMessage);
+ result.putString(ERRORS, errorJson.toString());
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+ return result;
+ }
+
+
+ private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage) {
+ try {
+ jsonObject.put(ERRORS, errorMessage);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage, String errorId, String initialAction) {
+ try {
+ jsonObject.putOpt(ERRORS, errorMessage);
+ jsonObject.putOpt(ERRORID, errorId);
+ jsonObject.putOpt(INITIAL_ACTION, initialAction);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ protected void sendToReceiverOrBroadcast(ResultReceiver receiver, int resultCode, Bundle resultData, Provider provider) {
+ if (resultData == null || resultData == Bundle.EMPTY) {
+ resultData = new Bundle();
+ }
+ resultData.putParcelable(PROVIDER_KEY, provider);
+ if (receiver != null) {
+ receiver.send(resultCode, resultData);
+ } else {
+ broadcastEvent(resultCode, resultData);
+ }
+ handleEventSummaryErrorLog(resultCode);
+ }
+
+ private void broadcastEvent(int resultCode , Bundle resultData) {
+ Intent intentUpdate = new Intent(BROADCAST_PROVIDER_API_EVENT);
+ intentUpdate.addCategory(Intent.CATEGORY_DEFAULT);
+ intentUpdate.putExtra(BROADCAST_RESULT_CODE, resultCode);
+ intentUpdate.putExtra(BROADCAST_RESULT_KEY, resultData);
+ serviceCallback.broadcastEvent(intentUpdate);
+ }
+
+ private void handleEventSummaryErrorLog(int resultCode) {
+ String event = null;
+ switch (resultCode) {
+ case INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
+ event = "download of vpn certificate.";
+ break;
+ case PROVIDER_NOK:
+ event = "setup or update provider details.";
+ break;
+ case INCORRECTLY_DOWNLOADED_EIP_SERVICE:
+ event = "update eip-service.json";
+ break;
+ case INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE:
+ event = "update invalid vpn certificate.";
+ break;
+ case INCORRECTLY_DOWNLOADED_GEOIP_JSON:
+ event = "download menshen service json.";
+ break;
+ case TOR_TIMEOUT:
+ case TOR_EXCEPTION:
+ event = "start tor for censorship circumvention";
+ break;
+ default:
+ break;
+ }
+ if (event != null) {
+ VpnStatus.logWarning("[API] failed provider API event: " + event);
+ }
+ }
+
+ String formatErrorMessage(final int errorStringId) {
+ return formatErrorMessage(getProviderFormattedString(resources, errorStringId));
+ }
+
+ private String formatErrorMessage(String errorMessage) {
+ return "{ \"" + ERRORS + "\" : \"" + errorMessage + "\" }";
+ }
+
+ private JSONObject getErrorMessageAsJson(final int toastStringId) {
+ try {
+ return new JSONObject(formatErrorMessage(toastStringId));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ return new JSONObject();
+ }
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManager.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManager.java
new file mode 100644
index 00000000..79c6f5c4
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManager.java
@@ -0,0 +1,167 @@
+package se.leap.bitmaskclient.providersetup;
+
+import static se.leap.bitmaskclient.R.string.malformed_url;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.DELAY;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.MISSING_NETWORK_CONNECTION;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.PARAMETERS;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.PROVIDER_NOK;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.RECEIVER_KEY;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.TOR_EXCEPTION;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.TOR_TIMEOUT;
+import static se.leap.bitmaskclient.providersetup.ProviderApiManagerV5.PROXY_HOST;
+import static se.leap.bitmaskclient.providersetup.ProviderApiManagerV5.SOCKS_PROXY_SCHEME;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_TOR_TIMEOUT;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupObservable.DOWNLOADED_PROVIDER_JSON;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus.OFF;
+
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import androidx.core.content.IntentCompat;
+
+import org.jetbrains.annotations.Blocking;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.concurrent.TimeoutException;
+
+import de.blinkt.openvpn.core.VpnStatus;
+import mobile.BitmaskMobile;
+import se.leap.bitmaskclient.BuildConfig;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.base.utils.ConfigHelper;
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import se.leap.bitmaskclient.tor.TorStatusObservable;
+
+public class ProviderApiManager extends ProviderApiManagerBase {
+ public static final String TAG = ProviderApiManager.class.getSimpleName();
+ public final ProviderApiManagerFactory versionedApiFactory;
+
+ public ProviderApiManager(Resources resources, ProviderApiManagerBase.ProviderApiServiceCallback callback) {
+ super(resources, callback);
+ this.versionedApiFactory = new ProviderApiManagerFactory(resources, callback);
+ }
+
+ @Blocking
+ public void handleIntent(Intent command) {
+ ResultReceiver receiver = null;
+ if (command.getParcelableExtra(RECEIVER_KEY) != null) {
+ receiver = command.getParcelableExtra(RECEIVER_KEY);
+ }
+ String action = command.getAction();
+ Bundle parameters = command.getBundleExtra(PARAMETERS);
+
+ if (action == null) {
+ Log.e(TAG, "Intent without action sent!");
+ return;
+ }
+
+ Provider provider = null;
+ if (command.hasExtra(PROVIDER_KEY)) {
+ provider = IntentCompat.getParcelableExtra(command, PROVIDER_KEY, Provider.class);
+ } else {
+ //TODO: consider returning error back e.g. NO_PROVIDER
+ Log.e(TAG, action + " called without provider!");
+ return;
+ }
+
+ if (parameters.containsKey(DELAY)) {
+ try {
+ Thread.sleep(parameters.getLong(DELAY));
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (!serviceCallback.hasNetworkConnection()) {
+ Bundle result = new Bundle();
+ eventSender.setErrorResult(result, R.string.error_network_connection, null);
+ eventSender.sendToReceiverOrBroadcast(receiver, MISSING_NETWORK_CONNECTION, result, provider);
+ return;
+ }
+ Bundle result = new Bundle();
+
+ try {
+ if (PreferenceHelper.hasSnowflakePrefs() && !VpnStatus.isVPNActive()) {
+ torHandler.startTorProxy();
+ }
+ } catch (InterruptedException | IllegalStateException e) {
+ e.printStackTrace();
+ eventSender.setErrorResultAction(result, action);
+ eventSender.sendToReceiverOrBroadcast(receiver, TOR_EXCEPTION, result, provider);
+ return;
+ } catch (TimeoutException e) {
+ torHandler.stopTorProxy();
+ eventSender.setErrorResult(result, R.string.error_tor_timeout, ERROR_TOR_TIMEOUT.toString(), action);
+ eventSender.sendToReceiverOrBroadcast(receiver, TOR_TIMEOUT, result, provider);
+ return;
+ }
+
+ if (!provider.hasDefinition()) {
+ result = downloadProviderDefinition(result, provider);
+ if (result.containsKey(ERRORS)) {
+ eventSender.sendToReceiverOrBroadcast(receiver, PROVIDER_NOK, result, provider);
+ return;
+ }
+ }
+
+ IProviderApiManager apiManager = versionedApiFactory.getProviderApiManager(provider);
+ apiManager.handleAction(action, provider, parameters, receiver);
+ }
+
+ private Bundle downloadProviderDefinition(Bundle result, Provider provider) {
+ getPersistedProviderUpdates(provider);
+ if (provider.hasDefinition()) {
+ return result;
+ }
+
+ try {
+ String providerString = fetch(provider, true);
+ if (ConfigHelper.checkErroneousDownload(providerString) || !isValidJson(providerString)) {
+ return eventSender.setErrorResult(result, malformed_url, null);
+ }
+
+ JSONObject jsonObject = new JSONObject(providerString);
+ provider.define(jsonObject);
+ provider.setModelsProvider(providerString);
+ ProviderSetupObservable.updateProgress(DOWNLOADED_PROVIDER_JSON);
+ } catch (Exception e) {
+ return eventSender.setErrorResult(result, R.string.malformed_url, null);
+ }
+
+ return result;
+ }
+
+ private String fetch(Provider provider, Boolean allowRetry) {
+ BitmaskMobile bm;
+ try {
+ bm = new BitmaskMobile(provider.getMainUrl(), new PreferenceHelper.SharedPreferenceStore());
+ bm.setDebug(BuildConfig.DEBUG);
+ if (TorStatusObservable.isRunning() && TorStatusObservable.getSocksProxyPort() != -1) {
+ bm.setSocksProxy(SOCKS_PROXY_SCHEME + PROXY_HOST + ":" + TorStatusObservable.getSocksProxyPort());
+ } else if (provider.hasIntroducer()) {
+ bm.setIntroducer(provider.getIntroducer().toUrl());
+ }
+ return bm.getProvider();
+ } catch (Exception e) {
+ e.printStackTrace();
+ try {
+ if (allowRetry &&
+ TorStatusObservable.getStatus() == OFF &&
+ torHandler.startTorProxy()
+ ) {
+ return fetch(provider, false);
+ }
+ } catch (InterruptedException | TimeoutException ex) {
+ ex.printStackTrace();
+ }
+ }
+ return null;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java
index ae55f81c..25a9fcce 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java
@@ -17,24 +17,10 @@
package se.leap.bitmaskclient.providersetup;
-import static se.leap.bitmaskclient.R.string.certificate_error;
-import static se.leap.bitmaskclient.R.string.error_io_exception_user_message;
-import static se.leap.bitmaskclient.R.string.error_json_exception_user_message;
-import static se.leap.bitmaskclient.R.string.error_no_such_algorithm_exception_user_message;
-import static se.leap.bitmaskclient.R.string.malformed_url;
-import static se.leap.bitmaskclient.R.string.server_unreachable_message;
-import static se.leap.bitmaskclient.R.string.service_is_down_error;
-import static se.leap.bitmaskclient.R.string.vpn_certificate_is_invalid;
-import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_cert;
-import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_details;
-import static se.leap.bitmaskclient.R.string.warning_expired_provider_cert;
-import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_PROVIDER_API_EVENT;
-import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_CODE;
-import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_KEY;
-import static se.leap.bitmaskclient.base.models.Constants.CREDENTIALS_PASSWORD;
-import static se.leap.bitmaskclient.base.models.Constants.CREDENTIALS_USERNAME;
-import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START;
-import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MODELS_BRIDGES;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MODELS_EIPSERVICE;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MODELS_GATEWAYS;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MODELS_PROVIDER;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_HASHES;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_LAST_SEEN;
@@ -45,115 +31,28 @@ import static se.leap.bitmaskclient.base.models.Provider.CA_CERT;
import static se.leap.bitmaskclient.base.models.Provider.GEOIP_URL;
import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_API_IP;
import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_IP;
-import static se.leap.bitmaskclient.base.utils.ConfigHelper.getTorTimeout;
-import static se.leap.bitmaskclient.base.utils.RSAHelper.parseRsaKeyFromString;
-import static se.leap.bitmaskclient.base.utils.ConfigHelper.getDomainFromMainURL;
import static se.leap.bitmaskclient.base.utils.CertificateHelper.getFingerprintFromCertificate;
-import static se.leap.bitmaskclient.base.utils.ConfigHelper.getProviderFormattedString;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.deleteProviderDetailsFromPreferences;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.getDomainFromMainURL;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getFromPersistedProvider;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getLongFromPersistedProvider;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getStringSetFromPersistedProvider;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.BACKEND_ERROR_KEY;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.BACKEND_ERROR_MESSAGE;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_EIP_SERVICE;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_GEOIP_JSON;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.DELAY;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_GEOIP_JSON;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_MOTD;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_SERVICE_JSON;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORID;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.FAILED_LOGIN;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.FAILED_SIGNUP;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_GEOIP_JSON;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.INITIAL_ACTION;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.LOGOUT_FAILED;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.LOG_IN;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.LOG_OUT;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.MISSING_NETWORK_CONNECTION;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.PARAMETERS;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.PROVIDER_NOK;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.PROVIDER_OK;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.QUIETLY_UPDATE_VPN_CERTIFICATE;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.RECEIVER_KEY;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.SET_UP_PROVIDER;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.SIGN_UP;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.SUCCESSFUL_LOGIN;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.SUCCESSFUL_LOGOUT;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.SUCCESSFUL_SIGNUP;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.TOR_EXCEPTION;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.TOR_TIMEOUT;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_PROVIDER_DETAILS;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.USER_MESSAGE;
-import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING;
-import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON;
-import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_INVALID_CERTIFICATE;
-import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_TOR_TIMEOUT;
-import static se.leap.bitmaskclient.providersetup.ProviderSetupObservable.DOWNLOADED_GEOIP_JSON;
-import static se.leap.bitmaskclient.providersetup.ProviderSetupObservable.DOWNLOADED_VPN_CERTIFICATE;
-import static se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus.OFF;
-import static se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus.ON;
-import static se.leap.bitmaskclient.tor.TorStatusObservable.getProxyPort;
import android.content.Intent;
import android.content.res.Resources;
-import android.os.Bundle;
-import android.os.ResultReceiver;
-import android.util.Base64;
-import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
import org.json.JSONException;
import org.json.JSONObject;
-import java.io.IOException;
-import java.math.BigInteger;
-import java.net.ConnectException;
-import java.net.MalformedURLException;
-import java.net.SocketTimeoutException;
-import java.net.UnknownHostException;
-import java.net.UnknownServiceException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
-import java.security.cert.CertificateException;
-import java.security.cert.CertificateExpiredException;
-import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
-import java.security.interfaces.RSAPrivateKey;
import java.util.ArrayList;
-import java.util.List;
-import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.TimeoutException;
-import javax.net.ssl.SSLHandshakeException;
-import javax.net.ssl.SSLPeerUnverifiedException;
-
-import de.blinkt.openvpn.core.VpnStatus;
-import okhttp3.OkHttpClient;
-import se.leap.bitmaskclient.R;
-import se.leap.bitmaskclient.base.models.Constants.CREDENTIAL_ERRORS;
import se.leap.bitmaskclient.base.models.Provider;
-import se.leap.bitmaskclient.base.models.ProviderObservable;
import se.leap.bitmaskclient.base.utils.ConfigHelper;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
-import se.leap.bitmaskclient.eip.EipStatus;
-import se.leap.bitmaskclient.motd.MotdClient;
-import se.leap.bitmaskclient.providersetup.connectivity.OkHttpClientGenerator;
-import se.leap.bitmaskclient.providersetup.models.LeapSRPSession;
-import se.leap.bitmaskclient.providersetup.models.SrpCredentials;
-import se.leap.bitmaskclient.providersetup.models.SrpRegistrationData;
-import se.leap.bitmaskclient.tor.TorStatusObservable;
/**
* Implements the logic of the http api calls. The methods of this class needs to be called from
@@ -163,738 +62,37 @@ import se.leap.bitmaskclient.tor.TorStatusObservable;
public abstract class ProviderApiManagerBase {
private final static String TAG = ProviderApiManagerBase.class.getName();
+ public static final String PROXY_HOST = "127.0.0.1";
+ public static final String SOCKS_PROXY_SCHEME = "socks5://";
public interface ProviderApiServiceCallback {
void broadcastEvent(Intent intent);
boolean startTorService() throws InterruptedException, IllegalStateException, TimeoutException;
void stopTorService();
int getTorHttpTunnelPort();
+ int getTorSocksProxyPort();
boolean hasNetworkConnection();
void saveProvider(Provider p);
}
- private final ProviderApiServiceCallback serviceCallback;
+ protected final ProviderApiServiceCallback serviceCallback;
protected Resources resources;
- OkHttpClientGenerator clientGenerator;
- ProviderApiManagerBase(Resources resources, OkHttpClientGenerator clientGenerator, ProviderApiServiceCallback callback) {
+ protected ProviderApiEventSender eventSender;
+ protected ProviderApiTorHandler torHandler;
+
+ ProviderApiManagerBase(Resources resources, ProviderApiServiceCallback callback) {
this.resources = resources;
this.serviceCallback = callback;
- this.clientGenerator = clientGenerator;
- }
-
- public void handleIntent(Intent command) {
- ResultReceiver receiver = null;
- if (command.getParcelableExtra(RECEIVER_KEY) != null) {
- receiver = command.getParcelableExtra(RECEIVER_KEY);
- }
- String action = command.getAction();
- Bundle parameters = command.getBundleExtra(PARAMETERS);
-
- if (action == null) {
- Log.e(TAG, "Intent without action sent!");
- return;
- }
-
- Provider provider = null;
- if (command.getParcelableExtra(PROVIDER_KEY) != null) {
- provider = command.getParcelableExtra(PROVIDER_KEY);
- } else {
- //TODO: consider returning error back e.g. NO_PROVIDER
- Log.e(TAG, action +" called without provider!");
- return;
- }
-
- if (parameters.containsKey(DELAY)) {
- try {
- Thread.sleep(parameters.getLong(DELAY));
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- if (!serviceCallback.hasNetworkConnection()) {
- Bundle result = new Bundle();
- setErrorResult(result, R.string.error_network_connection, null);
- sendToReceiverOrBroadcast(receiver, MISSING_NETWORK_CONNECTION, result, provider);
- return;
- }
-
- try {
- if (PreferenceHelper.hasSnowflakePrefs() && !VpnStatus.isVPNActive()) {
- startTorProxy();
- }
- } catch (InterruptedException | IllegalStateException e) {
- e.printStackTrace();
- Bundle result = new Bundle();
- setErrorResultAction(result, action);
- sendToReceiverOrBroadcast(receiver, TOR_EXCEPTION, result, provider);
- return;
- } catch (TimeoutException e) {
- serviceCallback.stopTorService();
- Bundle result = new Bundle();
- setErrorResult(result, R.string.error_tor_timeout, ERROR_TOR_TIMEOUT.toString(), action);
- sendToReceiverOrBroadcast(receiver, TOR_TIMEOUT, result, provider);
- return;
- }
-
- Bundle result = new Bundle();
- switch (action) {
- case UPDATE_PROVIDER_DETAILS:
- ProviderObservable.getInstance().setProviderForDns(provider);
- resetProviderDetails(provider);
- Bundle task = new Bundle();
- result = setUpProvider(provider, task);
- if (result.getBoolean(BROADCAST_RESULT_KEY)) {
- getGeoIPJson(provider);
- sendToReceiverOrBroadcast(receiver, PROVIDER_OK, result, provider);
- } else {
- sendToReceiverOrBroadcast(receiver, PROVIDER_NOK, result, provider);
- }
- ProviderObservable.getInstance().setProviderForDns(null);
- break;
-
- case SET_UP_PROVIDER:
- ProviderObservable.getInstance().setProviderForDns(provider);
- result = setUpProvider(provider, parameters);
- if (result.getBoolean(BROADCAST_RESULT_KEY)) {
- getGeoIPJson(provider);
- if (provider.hasGeoIpJson()) {
- ProviderSetupObservable.updateProgress(DOWNLOADED_GEOIP_JSON);
- }
- sendToReceiverOrBroadcast(receiver, PROVIDER_OK, result, provider);
- } else {
- sendToReceiverOrBroadcast(receiver, PROVIDER_NOK, result, provider);
- }
- ProviderObservable.getInstance().setProviderForDns(null);
- break;
- case SIGN_UP:
- result = tryToRegister(provider, parameters);
- if (result.getBoolean(BROADCAST_RESULT_KEY)) {
- sendToReceiverOrBroadcast(receiver, SUCCESSFUL_SIGNUP, result, provider);
- } else {
- sendToReceiverOrBroadcast(receiver, FAILED_SIGNUP, result, provider);
- }
- break;
- case LOG_IN:
- result = tryToAuthenticate(provider, parameters);
- if (result.getBoolean(BROADCAST_RESULT_KEY)) {
- sendToReceiverOrBroadcast(receiver, SUCCESSFUL_LOGIN, result, provider);
- } else {
- sendToReceiverOrBroadcast(receiver, FAILED_LOGIN, result, provider);
- }
- break;
- case LOG_OUT:
- if (logOut(provider)) {
- sendToReceiverOrBroadcast(receiver, SUCCESSFUL_LOGOUT, Bundle.EMPTY, provider);
- } else {
- sendToReceiverOrBroadcast(receiver, LOGOUT_FAILED, Bundle.EMPTY, provider);
- }
- break;
- case DOWNLOAD_VPN_CERTIFICATE:
- ProviderObservable.getInstance().setProviderForDns(provider);
- result = updateVpnCertificate(provider);
- if (result.getBoolean(BROADCAST_RESULT_KEY)) {
- serviceCallback.saveProvider(provider);
- ProviderSetupObservable.updateProgress(DOWNLOADED_VPN_CERTIFICATE);
- sendToReceiverOrBroadcast(receiver, CORRECTLY_DOWNLOADED_VPN_CERTIFICATE, result, provider);
- } else {
- sendToReceiverOrBroadcast(receiver, INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE, result, provider);
- }
- ProviderObservable.getInstance().setProviderForDns(null);
- break;
- case QUIETLY_UPDATE_VPN_CERTIFICATE:
- ProviderObservable.getInstance().setProviderForDns(provider);
- result = updateVpnCertificate(provider);
- if (result.getBoolean(BROADCAST_RESULT_KEY)) {
- Log.d(TAG, "successfully downloaded VPN certificate");
- provider.setShouldUpdateVpnCertificate(false);
- PreferenceHelper.storeProviderInPreferences(provider);
- ProviderObservable.getInstance().updateProvider(provider);
- }
- ProviderObservable.getInstance().setProviderForDns(null);
- break;
- case DOWNLOAD_MOTD:
- MotdClient client = new MotdClient(provider);
- JSONObject motd = client.fetchJson();
- if (motd != null) {
- provider.setMotdJson(motd);
- provider.setLastMotdUpdate(System.currentTimeMillis());
- }
- PreferenceHelper.storeProviderInPreferences(provider);
- ProviderObservable.getInstance().updateProvider(provider);
- break;
-
- case UPDATE_INVALID_VPN_CERTIFICATE:
- ProviderObservable.getInstance().setProviderForDns(provider);
- result = updateVpnCertificate(provider);
- if (result.getBoolean(BROADCAST_RESULT_KEY)) {
- sendToReceiverOrBroadcast(receiver, CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE, result, provider);
- } else {
- sendToReceiverOrBroadcast(receiver, INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE, result, provider);
- }
- ProviderObservable.getInstance().setProviderForDns(null);
- break;
- case DOWNLOAD_SERVICE_JSON:
- ProviderObservable.getInstance().setProviderForDns(provider);
- Log.d(TAG, "update eip service json");
- result = getAndSetEipServiceJson(provider);
- if (result.getBoolean(BROADCAST_RESULT_KEY)) {
- sendToReceiverOrBroadcast(receiver, CORRECTLY_DOWNLOADED_EIP_SERVICE, result, provider);
- } else {
- sendToReceiverOrBroadcast(receiver, INCORRECTLY_DOWNLOADED_EIP_SERVICE, result, provider);
- }
- ProviderObservable.getInstance().setProviderForDns(null);
- break;
- case DOWNLOAD_GEOIP_JSON:
- if (!provider.getGeoipUrl().isDefault()) {
- boolean startEIP = parameters.getBoolean(EIP_ACTION_START);
- ProviderObservable.getInstance().setProviderForDns(provider);
- result = getGeoIPJson(provider);
- result.putBoolean(EIP_ACTION_START, startEIP);
- if (result.getBoolean(BROADCAST_RESULT_KEY)) {
- sendToReceiverOrBroadcast(receiver, CORRECTLY_DOWNLOADED_GEOIP_JSON, result, provider);
- } else {
- sendToReceiverOrBroadcast(receiver, INCORRECTLY_DOWNLOADED_GEOIP_JSON, result, provider);
- }
- ProviderObservable.getInstance().setProviderForDns(null);
- }
- break;
- }
- }
-
- private void saveCustomProvider() {
-
- }
-
- protected boolean startTorProxy() throws InterruptedException, IllegalStateException, TimeoutException {
- if (EipStatus.getInstance().isDisconnected() &&
- PreferenceHelper.getUseSnowflake() &&
- serviceCallback.startTorService()) {
- waitForTorCircuits();
- if (TorStatusObservable.isCancelled()) {
- throw new InterruptedException("Cancelled Tor setup.");
- }
- int port = serviceCallback.getTorHttpTunnelPort();
- TorStatusObservable.setProxyPort(port);
- return port != -1;
- }
- return false;
- }
-
- private void waitForTorCircuits() throws InterruptedException, TimeoutException {
- if (TorStatusObservable.getStatus() == ON) {
- return;
- }
- TorStatusObservable.waitUntil(this::isTorOnOrCancelled, getTorTimeout());
- }
-
- private boolean isTorOnOrCancelled() {
- return TorStatusObservable.getStatus() == ON || TorStatusObservable.isCancelled();
+ this.eventSender = new ProviderApiEventSender(resources, serviceCallback);
+ this.torHandler = new ProviderApiTorHandler(callback);
}
void resetProviderDetails(Provider provider) {
provider.reset();
- deleteProviderDetailsFromPreferences(provider.getDomain());
}
- String formatErrorMessage(final int errorStringId) {
- return formatErrorMessage(getProviderFormattedString(resources, errorStringId));
- }
-
- private String formatErrorMessage(String errorMessage) {
- return "{ \"" + ERRORS + "\" : \"" + errorMessage + "\" }";
- }
-
- private JSONObject getErrorMessageAsJson(final int toastStringId) {
- try {
- return new JSONObject(formatErrorMessage(toastStringId));
- } catch (JSONException e) {
- e.printStackTrace();
- return new JSONObject();
- }
- }
-
- private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage) {
- try {
- jsonObject.put(ERRORS, errorMessage);
- } catch (JSONException e) {
- e.printStackTrace();
- }
- }
-
- private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage, String errorId, String initialAction) {
- try {
- jsonObject.putOpt(ERRORS, errorMessage);
- jsonObject.putOpt(ERRORID, errorId);
- jsonObject.putOpt(INITIAL_ACTION, initialAction);
- } catch (JSONException e) {
- e.printStackTrace();
- }
- }
-
- private Bundle tryToRegister(Provider provider, Bundle task) {
- Bundle result = new Bundle();
-
- String username = task.getString(CREDENTIALS_USERNAME);
- String password = task.getString(CREDENTIALS_PASSWORD);
-
- if(provider == null) {
- result.putBoolean(BROADCAST_RESULT_KEY, false);
- Log.e(TAG, "no provider when trying to register");
- return result;
- }
-
- if (validUserLoginData(username, password)) {
- result = register(provider, username, password);
- } else {
- if (!wellFormedPassword(password)) {
- result.putBoolean(BROADCAST_RESULT_KEY, false);
- result.putString(CREDENTIALS_USERNAME, username);
- result.putBoolean(CREDENTIAL_ERRORS.PASSWORD_INVALID_LENGTH.toString(), true);
- }
- if (!validUsername(username)) {
- result.putBoolean(BROADCAST_RESULT_KEY, false);
- result.putBoolean(CREDENTIAL_ERRORS.USERNAME_MISSING.toString(), true);
- }
- }
-
- return result;
- }
-
- private Bundle register(Provider provider, String username, String password) {
- JSONObject stepResult = null;
- OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), getProxyPort(), stepResult);
- if (okHttpClient == null) {
- return backendErrorNotification(stepResult, username);
- }
-
- LeapSRPSession client = new LeapSRPSession(username, password);
- byte[] salt = client.calculateNewSalt();
-
- BigInteger password_verifier = client.calculateV(username, password, salt);
-
- JSONObject api_result = sendNewUserDataToSRPServer(provider.getApiUrlWithVersion(), username, new BigInteger(1, salt).toString(16), password_verifier.toString(16), okHttpClient);
-
- Bundle result = new Bundle();
- if (api_result.has(ERRORS) || api_result.has(BACKEND_ERROR_KEY))
- result = backendErrorNotification(api_result, username);
- else {
- result.putString(CREDENTIALS_USERNAME, username);
- result.putString(CREDENTIALS_PASSWORD, password);
- result.putBoolean(BROADCAST_RESULT_KEY, true);
- }
-
- return result;
- }
-
- /**
- * Starts the authentication process using SRP protocol.
- *
- * @param task containing: username, password and provider
- * @return a bundle with a boolean value mapped to a key named BROADCAST_RESULT_KEY, and which is true if authentication was successful.
- */
- private Bundle tryToAuthenticate(Provider provider, Bundle task) {
- Bundle result = new Bundle();
-
- String username = task.getString(CREDENTIALS_USERNAME);
- String password = task.getString(CREDENTIALS_PASSWORD);
-
- if (validUserLoginData(username, password)) {
- result = authenticate(provider, username, password);
- } else {
- if (!wellFormedPassword(password)) {
- result.putBoolean(BROADCAST_RESULT_KEY, false);
- result.putString(CREDENTIALS_USERNAME, username);
- result.putBoolean(CREDENTIAL_ERRORS.PASSWORD_INVALID_LENGTH.toString(), true);
- }
- if (!validUsername(username)) {
- result.putBoolean(BROADCAST_RESULT_KEY, false);
- result.putBoolean(CREDENTIAL_ERRORS.USERNAME_MISSING.toString(), true);
- }
- }
-
- return result;
- }
-
- private Bundle authenticate(Provider provider, String username, String password) {
- Bundle result = new Bundle();
- JSONObject stepResult = new JSONObject();
-
- String providerApiUrl = provider.getApiUrlWithVersion();
-
- OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), getProxyPort(), stepResult);
- if (okHttpClient == null) {
- return backendErrorNotification(stepResult, username);
- }
-
- LeapSRPSession client = new LeapSRPSession(username, password);
- byte[] A = client.exponential();
-
- JSONObject step_result = sendAToSRPServer(providerApiUrl, username, new BigInteger(1, A).toString(16), okHttpClient);
- try {
- String salt = step_result.getString(LeapSRPSession.SALT);
- byte[] Bbytes = new BigInteger(step_result.getString("B"), 16).toByteArray();
- byte[] M1 = client.response(new BigInteger(salt, 16).toByteArray(), Bbytes);
- if (M1 != null) {
- step_result = sendM1ToSRPServer(providerApiUrl, username, M1, okHttpClient);
- setTokenIfAvailable(step_result);
- byte[] M2 = new BigInteger(step_result.getString(LeapSRPSession.M2), 16).toByteArray();
- if (client.verify(M2)) {
- result.putBoolean(BROADCAST_RESULT_KEY, true);
- } else {
- backendErrorNotification(step_result, username);
- }
- } else {
- result.putBoolean(BROADCAST_RESULT_KEY, false);
- result.putString(CREDENTIALS_USERNAME, username);
- result.putString(USER_MESSAGE, resources.getString(R.string.error_srp_math_error_user_message));
- }
- } catch (JSONException e) {
- result = backendErrorNotification(step_result, username);
- e.printStackTrace();
- }
-
- return result;
- }
-
- private boolean setTokenIfAvailable(JSONObject authentication_step_result) {
- try {
- LeapSRPSession.setToken(authentication_step_result.getString(LeapSRPSession.TOKEN));
- } catch (JSONException e) {
- return false;
- }
- return true;
- }
-
- private Bundle backendErrorNotification(JSONObject result, String username) {
- Bundle userNotificationBundle = new Bundle();
- if (result.has(ERRORS)) {
- Object baseErrorMessage = result.opt(ERRORS);
- if (baseErrorMessage instanceof JSONObject) {
- try {
- JSONObject errorMessage = result.getJSONObject(ERRORS);
- String errorType = errorMessage.keys().next().toString();
- String message = errorMessage.get(errorType).toString();
- userNotificationBundle.putString(USER_MESSAGE, message);
- } catch (JSONException | NoSuchElementException | NullPointerException e) {
- e.printStackTrace();
- }
- } else if (baseErrorMessage instanceof String) {
- try {
- String errorMessage = result.getString(ERRORS);
- userNotificationBundle.putString(USER_MESSAGE, errorMessage);
- } catch (JSONException e) {
- e.printStackTrace();
- }
- }
- } else if (result.has(BACKEND_ERROR_KEY)) {
- try {
- String backendErrorMessage = resources.getString(R.string.error_json_exception_user_message);
- if (result.has(BACKEND_ERROR_MESSAGE)) {
- backendErrorMessage = resources.getString(R.string.error) + result.getString(BACKEND_ERROR_MESSAGE);
- }
- userNotificationBundle.putString(USER_MESSAGE, backendErrorMessage);
- } catch (JSONException e) {
- e.printStackTrace();
- }
- }
-
- if (!username.isEmpty())
- userNotificationBundle.putString(CREDENTIALS_USERNAME, username);
- userNotificationBundle.putBoolean(BROADCAST_RESULT_KEY, false);
-
- return userNotificationBundle;
- }
-
- private void sendToReceiverOrBroadcast(ResultReceiver receiver, int resultCode, Bundle resultData, Provider provider) {
- if (resultData == null || resultData == Bundle.EMPTY) {
- resultData = new Bundle();
- }
- resultData.putParcelable(PROVIDER_KEY, provider);
- if (receiver != null) {
- receiver.send(resultCode, resultData);
- } else {
- broadcastEvent(resultCode, resultData);
- }
- handleEventSummaryErrorLog(resultCode);
- }
-
- private void broadcastEvent(int resultCode , Bundle resultData) {
- Intent intentUpdate = new Intent(BROADCAST_PROVIDER_API_EVENT);
- intentUpdate.addCategory(Intent.CATEGORY_DEFAULT);
- intentUpdate.putExtra(BROADCAST_RESULT_CODE, resultCode);
- intentUpdate.putExtra(BROADCAST_RESULT_KEY, resultData);
- serviceCallback.broadcastEvent(intentUpdate);
- }
-
- private void handleEventSummaryErrorLog(int resultCode) {
- String event = null;
- switch (resultCode) {
- case FAILED_LOGIN:
- event = "login.";
- break;
- case FAILED_SIGNUP:
- event = "signup.";
- break;
- case SUCCESSFUL_LOGOUT:
- event = "logout.";
- break;
- case INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
- event = "download of vpn certificate.";
- break;
- case PROVIDER_NOK:
- event = "setup or update provider details.";
- break;
- case INCORRECTLY_DOWNLOADED_EIP_SERVICE:
- event = "update eip-service.json";
- break;
- case INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE:
- event = "update invalid vpn certificate.";
- break;
- case INCORRECTLY_DOWNLOADED_GEOIP_JSON:
- event = "download menshen service json.";
- break;
- case TOR_TIMEOUT:
- case TOR_EXCEPTION:
- event = "start tor for censorship circumvention";
- break;
- default:
- break;
- }
- if (event != null) {
- VpnStatus.logWarning("[API] failed provider API event: " + event);
- }
- }
-
- /**
- * Validates parameters entered by the user to log in
- *
- * @param username
- * @param password
- * @return true if both parameters are present and the entered password length is greater or equal to eight (8).
- */
- private boolean validUserLoginData(String username, String password) {
- return validUsername(username) && wellFormedPassword(password);
- }
-
- private boolean validUsername(String username) {
- return username != null && !username.isEmpty();
- }
-
- /**
- * Validates a password
- *
- * @param password
- * @return true if the entered password length is greater or equal to eight (8).
- */
- private boolean wellFormedPassword(String password) {
- return password != null && password.length() >= 8;
- }
-
- /**
- * Sends an HTTP POST request to the authentication server with the SRP Parameter A.
- *
- * @param server_url
- * @param username
- * @param clientA First SRP parameter sent
- * @param okHttpClient
- * @return response from authentication server
- */
- private JSONObject sendAToSRPServer(String server_url, String username, String clientA, OkHttpClient okHttpClient) {
- SrpCredentials srpCredentials = new SrpCredentials(username, clientA);
- return sendToServer(server_url + "/sessions.json", "POST", srpCredentials.toString(), okHttpClient);
- }
-
- /**
- * Sends an HTTP PUT request to the authentication server with the SRP Parameter M1 (or simply M).
- *
- * @param server_url
- * @param username
- * @param m1 Second SRP parameter sent
- * @param okHttpClient
- * @return response from authentication server
- */
- private JSONObject sendM1ToSRPServer(String server_url, String username, byte[] m1, OkHttpClient okHttpClient) {
- String m1json = "{\"client_auth\":\"" + new BigInteger(1, ConfigHelper.trim(m1)).toString(16)+ "\"}";
- return sendToServer(server_url + "/sessions/" + username + ".json", "PUT", m1json, okHttpClient);
- }
-
- /**
- * Sends an HTTP POST request to the api server to register a new user.
- *
- * @param server_url
- * @param username
- * @param salt
- * @param password_verifier
- * @param okHttpClient
- * @return response from authentication server
- */
- private JSONObject sendNewUserDataToSRPServer(String server_url, String username, String salt, String password_verifier, OkHttpClient okHttpClient) {
- return sendToServer(server_url + "/users.json", "POST", new SrpRegistrationData(username, salt, password_verifier).toString(), okHttpClient);
- }
-
- /**
- * Executes an HTTP request expecting a JSON response.
- *
- * @param url
- * @param request_method
- * @return response from authentication server
- */
- private JSONObject sendToServer(String url, String request_method, String jsonString, OkHttpClient okHttpClient) {
- return requestJsonFromServer(url, request_method, jsonString, new ArrayList<Pair<String, String>>(), okHttpClient);
- }
-
- protected String sendGetStringToServer(@NonNull String url, @NonNull List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) {
- return requestStringFromServer(url, "GET", null, headerArgs, okHttpClient);
- }
-
-
-
- private JSONObject requestJsonFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) {
- JSONObject responseJson;
- String plain_response = requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient);
-
- try {
- responseJson = new JSONObject(plain_response);
- } catch (NullPointerException | JSONException e) {
- e.printStackTrace();
- responseJson = getErrorMessageAsJson(error_json_exception_user_message);
- VpnStatus.logWarning("[API] got null response for request: " + url);
- }
- return responseJson;
-
- }
-
- private String requestStringFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) {
- String plainResponseBody;
-
- try {
-
- plainResponseBody = ProviderApiConnector.requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient);
-
- } catch (NullPointerException npe) {
- plainResponseBody = formatErrorMessage(error_json_exception_user_message);
- VpnStatus.logWarning("[API] Null response body for request " + url + ": " + npe.getLocalizedMessage());
- } catch (UnknownHostException | SocketTimeoutException e) {
- plainResponseBody = formatErrorMessage(server_unreachable_message);
- VpnStatus.logWarning("[API] UnknownHostException or SocketTimeoutException for request " + url + ": " + e.getLocalizedMessage());
- } catch (MalformedURLException e) {
- plainResponseBody = formatErrorMessage(malformed_url);
- VpnStatus.logWarning("[API] MalformedURLException for request " + url + ": " + e.getLocalizedMessage());
- } catch (SSLHandshakeException | SSLPeerUnverifiedException e) {
- plainResponseBody = formatErrorMessage(certificate_error);
- VpnStatus.logWarning("[API] SSLHandshakeException or SSLPeerUnverifiedException for request " + url + ": " + e.getLocalizedMessage());
- } catch (ConnectException e) {
- plainResponseBody = formatErrorMessage(service_is_down_error);
- VpnStatus.logWarning("[API] ConnectException for request " + url + ": " + e.getLocalizedMessage());
- } catch (IllegalArgumentException e) {
- plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message);
- VpnStatus.logWarning("[API] IllegalArgumentException for request " + url + ": " + e.getLocalizedMessage());
- } catch (UnknownServiceException e) {
- //unable to find acceptable protocols - tlsv1.2 not enabled?
- plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message);
- VpnStatus.logWarning("[API] UnknownServiceException for request " + url + ": " + e.getLocalizedMessage());
- } catch (IOException e) {
- plainResponseBody = formatErrorMessage(error_io_exception_user_message);
- VpnStatus.logWarning("[API] IOException for request " + url + ": " + e.getLocalizedMessage());
- }
-
- return plainResponseBody;
- }
-
- private boolean canConnect(Provider provider, Bundle result) {
- return canConnect(provider, result, 0);
- }
-
- private boolean canConnect(Provider provider, Bundle result, int tries) {
- JSONObject errorJson = new JSONObject();
- String providerUrl = provider.getApiUrlString() + "/provider.json";
-
- OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), getProxyPort(), errorJson);
- if (okHttpClient == null) {
- result.putString(ERRORS, errorJson.toString());
- return false;
- }
-
- if (tries > 0) {
- result.remove(ERRORS);
- }
-
- try {
- return ProviderApiConnector.canConnect(okHttpClient, providerUrl);
-
- } catch (UnknownHostException | SocketTimeoutException e) {
- VpnStatus.logWarning("[API] UnknownHostException or SocketTimeoutException during connection check: " + e.getLocalizedMessage());
- setErrorResult(result, server_unreachable_message, null);
- } catch (MalformedURLException e) {
- VpnStatus.logWarning("[API] MalformedURLException during connection check: " + e.getLocalizedMessage());
- setErrorResult(result, malformed_url, null);
- } catch (SSLHandshakeException e) {
- VpnStatus.logWarning("[API] SSLHandshakeException during connection check: " + e.getLocalizedMessage());
- setErrorResult(result, warning_corrupted_provider_cert, ERROR_INVALID_CERTIFICATE.toString());
- } catch (ConnectException e) {
- VpnStatus.logWarning("[API] ConnectException during connection check: " + e.getLocalizedMessage());
- setErrorResult(result, service_is_down_error, null);
- } catch (IllegalArgumentException e) {
- VpnStatus.logWarning("[API] IllegalArgumentException during connection check: " + e.getLocalizedMessage());
- setErrorResult(result, error_no_such_algorithm_exception_user_message, null);
- } catch (UnknownServiceException e) {
- VpnStatus.logWarning("[API] UnknownServiceException during connection check: " + e.getLocalizedMessage());
- //unable to find acceptable protocols - tlsv1.2 not enabled?
- setErrorResult(result, error_no_such_algorithm_exception_user_message, null);
- } catch (IOException e) {
- VpnStatus.logWarning("[API] IOException during connection check: " + e.getLocalizedMessage());
- setErrorResult(result, error_io_exception_user_message, null);
- }
-
- try {
- if (tries == 0 &&
- result.containsKey(ERRORS) &&
- TorStatusObservable.getStatus() == OFF &&
- startTorProxy()
- ) {
- return canConnect(provider, result, 1);
- }
- } catch (InterruptedException | IllegalStateException | TimeoutException e) {
- e.printStackTrace();
- }
-
- return false;
- }
-
- /**
- * Downloads a provider.json from a given URL, adding a new provider using the given name.
- *
- * @param task containing a boolean meaning if the provider is custom or not, another boolean meaning if the user completely trusts this provider
- * @return a bundle with a boolean value mapped to a key named BROADCAST_RESULT_KEY, and which is true if the update was successful.
- */
- protected abstract Bundle setUpProvider(Provider provider, Bundle task);
-
- /**
- * Downloads the eip-service.json from a given URL, and saves eip service capabilities including the offered gateways
- * @return a bundle with a boolean value mapped to a key named BROADCAST_RESULT_KEY, and which is true if the download was successful.
- */
- protected abstract Bundle getAndSetEipServiceJson(Provider provider);
-
- /**
- * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate.
- *
- * @return true if certificate was downloaded correctly, false if provider.json is not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error.
- */
- protected abstract Bundle updateVpnCertificate(Provider provider);
-
-
- /**
- * Fetches the Geo ip Json, containing a list of gateways sorted by distance from the users current location
- *
- * @param provider
- * @return
- */
- protected abstract Bundle getGeoIPJson(Provider provider);
-
-
protected boolean isValidJson(String jsonString) {
try {
new JSONObject(jsonString);
@@ -937,11 +135,14 @@ public abstract class ProviderApiManagerBase {
}
protected void getPersistedProviderUpdates(Provider provider) {
- String providerDomain = getDomainFromMainURL(provider.getMainUrlString());
+ String providerDomain = getDomainFromMainURL(provider.getMainUrl());
+ if (providerDomain == null) {
+ return;
+ }
if (hasUpdatedProviderDetails(providerDomain)) {
provider.setCaCert(getPersistedProviderCA(providerDomain));
provider.define(getPersistedProviderDefinition(providerDomain));
- provider.setPrivateKey(getPersistedPrivateKey(providerDomain));
+ provider.setPrivateKeyString(getPersistedPrivateKey(providerDomain));
provider.setVpnCertificate(getPersistedVPNCertificate(providerDomain));
provider.setProviderApiIp(getPersistedProviderApiIp(providerDomain));
provider.setProviderIp(getPersistedProviderIp(providerDomain));
@@ -950,103 +151,13 @@ public abstract class ProviderApiManagerBase {
provider.setMotdLastSeenHashes(getPersistedMotdHashes(providerDomain));
provider.setLastMotdUpdate(getPersistedMotdLastUpdate(providerDomain));
provider.setMotdJson(getPersistedMotd(providerDomain));
+ provider.setModelsProvider(getFromPersistedProvider(PROVIDER_MODELS_PROVIDER, providerDomain));
+ provider.setService(getFromPersistedProvider(PROVIDER_MODELS_EIPSERVICE, providerDomain));
+ provider.setGateways(getFromPersistedProvider(PROVIDER_MODELS_GATEWAYS, providerDomain));
+ provider.setBridges(getFromPersistedProvider(PROVIDER_MODELS_BRIDGES, providerDomain));
}
}
- Bundle validateProviderDetails(Provider provider) {
- Bundle result = new Bundle();
- result.putBoolean(BROADCAST_RESULT_KEY, false);
-
- if (!provider.hasDefinition()) {
- return result;
- }
-
- result = validateCertificateForProvider(result, provider);
-
- //invalid certificate or no certificate or unable to connect due other connectivity issues
- if (result.containsKey(ERRORS) || (result.containsKey(BROADCAST_RESULT_KEY) && !result.getBoolean(BROADCAST_RESULT_KEY)) ) {
- return result;
- }
-
- result.putBoolean(BROADCAST_RESULT_KEY, true);
-
- return result;
- }
-
- protected Bundle validateCertificateForProvider(Bundle result, Provider provider) {
- String caCert = provider.getCaCert();
-
- if (ConfigHelper.checkErroneousDownload(caCert)) {
- VpnStatus.logWarning("[API] No provider cert.");
- return result;
- }
-
- ArrayList<X509Certificate> certificates = ConfigHelper.parseX509CertificatesFromString(caCert);
- if (certificates == null) {
- return setErrorResult(result, warning_corrupted_provider_cert, ERROR_INVALID_CERTIFICATE.toString());
- }
- try {
- String encoding = provider.getCertificatePinEncoding();
- String expectedFingerprint = provider.getCertificatePin();
-
- // Do certificate pinning only if we have 1 cert, otherwise we assume some transitioning of
- // X509 certs, therefore we cannot do cert pinning
- if (certificates.size() == 1) {
- String realFingerprint = getFingerprintFromCertificate(certificates.get(0), encoding);
- if (!realFingerprint.trim().equalsIgnoreCase(expectedFingerprint.trim())) {
- return setErrorResult(result, warning_corrupted_provider_cert, ERROR_CERTIFICATE_PINNING.toString());
- }
- }
- for (X509Certificate certificate : certificates) {
- certificate.checkValidity();
- }
-
- if (!canConnect(provider, result)) {
- return result;
- }
- } catch (NoSuchAlgorithmException e ) {
- return setErrorResult(result, error_no_such_algorithm_exception_user_message, null);
- } catch (ArrayIndexOutOfBoundsException e) {
- return setErrorResult(result, warning_corrupted_provider_details, ERROR_CORRUPTED_PROVIDER_JSON.toString());
- } catch (CertificateEncodingException | CertificateNotYetValidException | CertificateExpiredException e) {
- return setErrorResult(result, warning_expired_provider_cert, ERROR_INVALID_CERTIFICATE.toString());
- }
-
- result.putBoolean(BROADCAST_RESULT_KEY, true);
- return result;
- }
-
- protected Bundle setErrorResult(Bundle result, String stringJsonErrorMessage) {
- String reasonToFail = pickErrorMessage(stringJsonErrorMessage);
- VpnStatus.logWarning("[API] error: " + reasonToFail);
- result.putString(ERRORS, reasonToFail);
- result.putBoolean(BROADCAST_RESULT_KEY, false);
- return result;
- }
-
- Bundle setErrorResultAction(Bundle result, String initialAction) {
- JSONObject errorJson = new JSONObject();
- addErrorMessageToJson(errorJson, null, null, initialAction);
- VpnStatus.logWarning("[API] error: " + initialAction + " failed.");
- result.putString(ERRORS, errorJson.toString());
- result.putBoolean(BROADCAST_RESULT_KEY, false);
- return result;
- }
-
- Bundle setErrorResult(Bundle result, int errorMessageId, String errorId) {
- return setErrorResult(result, errorMessageId, errorId, null);
- }
-
- Bundle setErrorResult(Bundle result, int errorMessageId, String errorId, String initialAction) {
- JSONObject errorJson = new JSONObject();
- String errorMessage = getProviderFormattedString(resources, errorMessageId);
- addErrorMessageToJson(errorJson, errorMessage, errorId, initialAction);
- VpnStatus.logWarning("[API] error: " + errorMessage);
- result.putString(ERRORS, errorJson.toString());
- result.putBoolean(BROADCAST_RESULT_KEY, false);
- return result;
- }
-
protected String getPersistedPrivateKey(String providerDomain) {
return getFromPersistedProvider(PROVIDER_PRIVATE_KEY, providerDomain);
}
@@ -1105,89 +216,4 @@ public abstract class ProviderApiManagerBase {
return PreferenceHelper.hasKey(Provider.KEY + "." + domain) && PreferenceHelper.hasKey(CA_CERT + "." + domain);
}
- /**
- * Interprets the error message as a JSON object and extract the "errors" keyword pair.
- * If the error message is not a JSON object, then it is returned untouched.
- *
- * @param stringJsonErrorMessage
- * @return final error message
- */
- protected String pickErrorMessage(String stringJsonErrorMessage) {
- String errorMessage = "";
- try {
- JSONObject jsonErrorMessage = new JSONObject(stringJsonErrorMessage);
- errorMessage = jsonErrorMessage.getString(ERRORS);
- } catch (JSONException e) {
- // TODO Auto-generated catch block
- errorMessage = stringJsonErrorMessage;
- } catch (NullPointerException e) {
- //do nothing
- }
-
- return errorMessage;
- }
-
- @NonNull
- protected List<Pair<String, String>> getAuthorizationHeader() {
- List<Pair<String, String>> headerArgs = new ArrayList<>();
- if (!LeapSRPSession.getToken().isEmpty()) {
- Pair<String, String> authorizationHeaderPair = new Pair<>(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken());
- headerArgs.add(authorizationHeaderPair);
- }
- return headerArgs;
- }
-
- private boolean logOut(Provider provider) {
- OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), getProxyPort(), new JSONObject());
- if (okHttpClient == null) {
- return false;
- }
-
- String deleteUrl = provider.getApiUrlWithVersion() + "/logout";
-
- try {
- if (ProviderApiConnector.delete(okHttpClient, deleteUrl)) {
- LeapSRPSession.setToken("");
- return true;
- }
- } catch (IOException e) {
- // eat me
- }
- return false;
- }
-
- protected Bundle loadCertificate(Provider provider, String certString) {
- Bundle result = new Bundle();
- if (certString == null) {
- setErrorResult(result, vpn_certificate_is_invalid, null);
- return result;
- }
-
- try {
- // API returns concatenated cert & key. Split them for OpenVPN options
- String certificateString = null, keyString = null;
- String[] certAndKey = certString.split("(?<=-\n)");
- for (int i = 0; i < certAndKey.length - 1; i++) {
- if (certAndKey[i].contains("KEY")) {
- keyString = certAndKey[i++] + certAndKey[i];
- } else if (certAndKey[i].contains("CERTIFICATE")) {
- certificateString = certAndKey[i++] + certAndKey[i];
- }
- }
-
- RSAPrivateKey key = parseRsaKeyFromString(keyString);
- keyString = Base64.encodeToString(key.getEncoded(), Base64.DEFAULT);
- provider.setPrivateKey( "-----BEGIN RSA PRIVATE KEY-----\n" + keyString + "-----END RSA PRIVATE KEY-----");
-
- ArrayList<X509Certificate> certificates = ConfigHelper.parseX509CertificatesFromString(certificateString);
- certificates.get(0).checkValidity();
- certificateString = Base64.encodeToString(certificates.get(0).getEncoded(), Base64.DEFAULT);
- provider.setVpnCertificate( "-----BEGIN CERTIFICATE-----\n" + certificateString + "-----END CERTIFICATE-----");
- result.putBoolean(BROADCAST_RESULT_KEY, true);
- } catch (CertificateException | NullPointerException e) {
- e.printStackTrace();
- setErrorResult(result, vpn_certificate_is_invalid, null);
- }
- return result;
- }
-}
+ }
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerFactory.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerFactory.java
new file mode 100644
index 00000000..3eae410f
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerFactory.java
@@ -0,0 +1,25 @@
+package se.leap.bitmaskclient.providersetup;
+
+import android.content.res.Resources;
+
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.providersetup.connectivity.OkHttpClientGenerator;
+
+public class ProviderApiManagerFactory {
+ private final Resources resources;
+ private final ProviderApiManagerBase.ProviderApiServiceCallback callback;
+ private static final String TAG = ProviderApiManagerFactory.class.getSimpleName();
+
+ public ProviderApiManagerFactory(Resources resources, ProviderApiManagerBase.ProviderApiServiceCallback callback) {
+ this.resources = resources;
+ this.callback = callback;
+ }
+
+ public IProviderApiManager getProviderApiManager(Provider provider) throws IllegalArgumentException {
+ if (provider.getApiVersion() >= 5) {
+ return new ProviderApiManagerV5(resources, callback);
+ }
+ OkHttpClientGenerator clientGenerator = new OkHttpClientGenerator(resources);
+ return new ProviderApiManagerV3(resources, clientGenerator, callback);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerV3.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerV3.java
new file mode 100644
index 00000000..965741f0
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerV3.java
@@ -0,0 +1,765 @@
+/**
+ * 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.providersetup;
+
+import static se.leap.bitmaskclient.BuildConfig.DEBUG_MODE;
+import static se.leap.bitmaskclient.R.string.certificate_error;
+import static se.leap.bitmaskclient.R.string.downloading_vpn_certificate_failed;
+import static se.leap.bitmaskclient.R.string.error_io_exception_user_message;
+import static se.leap.bitmaskclient.R.string.error_json_exception_user_message;
+import static se.leap.bitmaskclient.R.string.error_no_such_algorithm_exception_user_message;
+import static se.leap.bitmaskclient.R.string.malformed_url;
+import static se.leap.bitmaskclient.R.string.server_unreachable_message;
+import static se.leap.bitmaskclient.R.string.service_is_down_error;
+import static se.leap.bitmaskclient.R.string.setup_error_text;
+import static se.leap.bitmaskclient.R.string.setup_error_text_custom;
+import static se.leap.bitmaskclient.R.string.vpn_certificate_is_invalid;
+import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_cert;
+import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_details;
+import static se.leap.bitmaskclient.R.string.warning_expired_provider_cert;
+import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.base.utils.BuildConfigHelper.isDefaultBitmask;
+import static se.leap.bitmaskclient.base.utils.CertificateHelper.getFingerprintFromCertificate;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.getProviderFormattedString;
+import static se.leap.bitmaskclient.base.utils.PrivateKeyHelper.getPEMFormattedPrivateKey;
+import static se.leap.bitmaskclient.base.utils.PrivateKeyHelper.parsePrivateKeyFromString;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_EIP_SERVICE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_GEOIP_JSON;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_GEOIP_JSON;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_MOTD;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_SERVICE_JSON;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_GEOIP_JSON;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.PROVIDER_NOK;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.PROVIDER_OK;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.QUIETLY_UPDATE_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.SET_UP_PROVIDER;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_INVALID_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupObservable.DOWNLOADED_CA_CERT;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupObservable.DOWNLOADED_EIP_SERVICE_JSON;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupObservable.DOWNLOADED_GEOIP_JSON;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupObservable.DOWNLOADED_PROVIDER_JSON;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupObservable.DOWNLOADED_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus.OFF;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.getProxyPort;
+
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.util.Base64;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.MalformedURLException;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.net.UnknownServiceException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+
+import de.blinkt.openvpn.core.VpnStatus;
+import okhttp3.OkHttpClient;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.base.models.ProviderObservable;
+import se.leap.bitmaskclient.base.utils.ConfigHelper;
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import se.leap.bitmaskclient.eip.EIP;
+import se.leap.bitmaskclient.motd.MotdClient;
+import se.leap.bitmaskclient.providersetup.connectivity.OkHttpClientGenerator;
+import se.leap.bitmaskclient.providersetup.models.LeapSRPSession;
+import se.leap.bitmaskclient.tor.TorStatusObservable;
+
+/**
+ * Implements the logic of the provider api http requests. The methods of this class need to be called from
+ * a background thread.
+ */
+
+
+public class ProviderApiManagerV3 extends ProviderApiManagerBase implements IProviderApiManager {
+
+ private static final String TAG = ProviderApiManagerV3.class.getSimpleName();
+
+ OkHttpClientGenerator clientGenerator;
+
+ public ProviderApiManagerV3(Resources resources, OkHttpClientGenerator clientGenerator, ProviderApiServiceCallback callback) {
+ super(resources, callback);
+ this.clientGenerator = clientGenerator;
+ }
+
+ @Override
+ public void handleAction(String action, Provider provider, Bundle parameters, ResultReceiver receiver) {
+ Bundle result = new Bundle();
+ switch (action) {
+ case SET_UP_PROVIDER:
+ ProviderObservable.getInstance().setProviderForDns(provider);
+ result = setupProvider(provider, parameters);
+ if (result.getBoolean(BROADCAST_RESULT_KEY)) {
+ getGeoIPJson(provider);
+ if (provider.hasGeoIpJson()) {
+ ProviderSetupObservable.updateProgress(DOWNLOADED_GEOIP_JSON);
+ }
+ eventSender.sendToReceiverOrBroadcast(receiver, PROVIDER_OK, result, provider);
+ } else {
+ eventSender.sendToReceiverOrBroadcast(receiver, PROVIDER_NOK, result, provider);
+ }
+ ProviderObservable.getInstance().setProviderForDns(null);
+ break;
+ case DOWNLOAD_VPN_CERTIFICATE:
+ ProviderObservable.getInstance().setProviderForDns(provider);
+ result = updateVpnCertificate(provider);
+ if (result.getBoolean(BROADCAST_RESULT_KEY)) {
+ serviceCallback.saveProvider(provider);
+ ProviderSetupObservable.updateProgress(DOWNLOADED_VPN_CERTIFICATE);
+ eventSender.sendToReceiverOrBroadcast(receiver, CORRECTLY_DOWNLOADED_VPN_CERTIFICATE, result, provider);
+ } else {
+ eventSender.sendToReceiverOrBroadcast(receiver, INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE, result, provider);
+ }
+ ProviderObservable.getInstance().setProviderForDns(null);
+ break;
+ case QUIETLY_UPDATE_VPN_CERTIFICATE:
+ ProviderObservable.getInstance().setProviderForDns(provider);
+ result = updateVpnCertificate(provider);
+ if (result.getBoolean(BROADCAST_RESULT_KEY)) {
+ Log.d(TAG, "successfully downloaded VPN certificate");
+ provider.setShouldUpdateVpnCertificate(false);
+ PreferenceHelper.storeProviderInPreferences(provider);
+ ProviderObservable.getInstance().updateProvider(provider);
+ }
+ ProviderObservable.getInstance().setProviderForDns(null);
+ break;
+ case DOWNLOAD_MOTD:
+ MotdClient client = new MotdClient(provider);
+ JSONObject motd = client.fetchJson();
+ if (motd != null) {
+ provider.setMotdJson(motd);
+ provider.setLastMotdUpdate(System.currentTimeMillis());
+ }
+ PreferenceHelper.storeProviderInPreferences(provider);
+ ProviderObservable.getInstance().updateProvider(provider);
+ break;
+
+ case UPDATE_INVALID_VPN_CERTIFICATE:
+ ProviderObservable.getInstance().setProviderForDns(provider);
+ result = updateVpnCertificate(provider);
+ if (result.getBoolean(BROADCAST_RESULT_KEY)) {
+ eventSender.sendToReceiverOrBroadcast(receiver, CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE, result, provider);
+ } else {
+ eventSender.sendToReceiverOrBroadcast(receiver, INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE, result, provider);
+ }
+ ProviderObservable.getInstance().setProviderForDns(null);
+ break;
+ case DOWNLOAD_SERVICE_JSON:
+ ProviderObservable.getInstance().setProviderForDns(provider);
+ Log.d(TAG, "update eip service json");
+ result = getAndSetEipServiceJson(provider);
+ if (result.getBoolean(BROADCAST_RESULT_KEY)) {
+ eventSender.sendToReceiverOrBroadcast(receiver, CORRECTLY_DOWNLOADED_EIP_SERVICE, result, provider);
+ } else {
+ eventSender.sendToReceiverOrBroadcast(receiver, INCORRECTLY_DOWNLOADED_EIP_SERVICE, result, provider);
+ }
+ ProviderObservable.getInstance().setProviderForDns(null);
+ break;
+ case DOWNLOAD_GEOIP_JSON:
+ if (!provider.getGeoipUrl().isEmpty()) {
+ boolean startEIP = parameters.getBoolean(EIP_ACTION_START);
+ ProviderObservable.getInstance().setProviderForDns(provider);
+ result = getGeoIPJson(provider);
+ result.putBoolean(EIP_ACTION_START, startEIP);
+ if (result.getBoolean(BROADCAST_RESULT_KEY)) {
+ eventSender.sendToReceiverOrBroadcast(receiver, CORRECTLY_DOWNLOADED_GEOIP_JSON, result, provider);
+ } else {
+ eventSender.sendToReceiverOrBroadcast(receiver, INCORRECTLY_DOWNLOADED_GEOIP_JSON, result, provider);
+ }
+ ProviderObservable.getInstance().setProviderForDns(null);
+ }
+ break;
+ }
+ }
+
+ /**
+ * Downloads a provider.json from a given URL, adding a new provider using the given name.
+ *
+ * @param task containing a boolean meaning if the provider is custom or not, another boolean meaning if the user completely trusts this provider, the provider name and its provider.json url.
+ * @return a bundle with a boolean value mapped to a key named BROADCAST_RESULT_KEY, and which is true if the update was successful.
+ */
+ public Bundle setupProvider(Provider provider, Bundle task) {
+ Bundle currentDownload = new Bundle();
+
+ if (provider.getMainUrl().isEmpty()) {
+ currentDownload.putBoolean(BROADCAST_RESULT_KEY, false);
+ eventSender.setErrorResult(currentDownload, malformed_url, null);
+ VpnStatus.logWarning("[API] MainURL String is not set. Cannot setup provider.");
+ return currentDownload;
+ }
+
+ currentDownload = validateProviderDetails(provider);
+ //provider certificate invalid
+ if (currentDownload.containsKey(ERRORS)) {
+ currentDownload.putParcelable(PROVIDER_KEY, provider);
+ return currentDownload;
+ }
+
+ //no provider json or certificate available
+ if (currentDownload.containsKey(BROADCAST_RESULT_KEY) && !currentDownload.getBoolean(BROADCAST_RESULT_KEY)) {
+ resetProviderDetails(provider);
+ }
+
+ if (!provider.hasDefinition()) {
+ currentDownload = getAndSetProviderJson(provider);
+ }
+ if (provider.hasDefinition()) {
+ ProviderSetupObservable.updateProgress(DOWNLOADED_PROVIDER_JSON);
+ if (!provider.hasCaCert()) {
+ currentDownload = downloadCACert(provider);
+ }
+ if (provider.hasCaCert()) {
+ ProviderSetupObservable.updateProgress(DOWNLOADED_CA_CERT);
+ currentDownload = getAndSetEipServiceJson(provider);
+ }
+
+ if (provider.hasEIP() && !provider.allowsRegistered() && !provider.allowsAnonymous()) {
+ eventSender.setErrorResult(currentDownload, isDefaultBitmask() ? setup_error_text : setup_error_text_custom, null);
+ } else if (provider.hasEIP()) {
+ ProviderSetupObservable.updateProgress(DOWNLOADED_EIP_SERVICE_JSON);
+ }
+ }
+
+ return currentDownload;
+ }
+
+ private Bundle getAndSetProviderJson(Provider provider) {
+ Bundle result = new Bundle();
+
+ String providerDotJsonString;
+ if(provider.getDefinitionString().length() == 0 || provider.getCaCert().isEmpty()) {
+ String providerJsonUrl = provider.getMainUrl() + "/provider.json";
+ providerDotJsonString = downloadWithCommercialCA(providerJsonUrl, provider);
+ } else {
+ providerDotJsonString = downloadFromApiUrlWithProviderCA("/provider.json", provider);
+ }
+
+ if (ConfigHelper.checkErroneousDownload(providerDotJsonString) || !isValidJson(providerDotJsonString)) {
+ eventSender.setErrorResult(result, malformed_url, null);
+ return result;
+ }
+
+ if (DEBUG_MODE) {
+ VpnStatus.logDebug("[API] PROVIDER JSON: " + providerDotJsonString);
+ }
+ try {
+ JSONObject providerJson = new JSONObject(providerDotJsonString);
+
+ if (provider.define(providerJson)) {
+ result.putBoolean(BROADCAST_RESULT_KEY, true);
+ } else {
+ return eventSender.setErrorResult(result, warning_corrupted_provider_details, ERROR_CORRUPTED_PROVIDER_JSON.toString());
+ }
+
+ } catch (JSONException e) {
+ eventSender.setErrorResult(result, providerDotJsonString);
+ }
+ return result;
+ }
+
+ /**
+ * Downloads the eip-service.json from a given URL, and saves eip service capabilities including the offered gateways
+ * @return a bundle with a boolean value mapped to a key named BROADCAST_RESULT_KEY, and which is true if the download was successful.
+ */
+ private Bundle getAndSetEipServiceJson(Provider provider) {
+ Bundle result = new Bundle();
+ String eipServiceJsonString = "";
+ try {
+ String eipServiceUrl = provider.getApiUrlWithVersion() + "/" + EIP.SERVICE_API_PATH;
+ eipServiceJsonString = downloadWithProviderCA(provider.getCaCert(), eipServiceUrl);
+ if (DEBUG_MODE) {
+ VpnStatus.logDebug("[API] EIP SERVICE JSON: " + eipServiceJsonString);
+ }
+ JSONObject eipServiceJson = new JSONObject(eipServiceJsonString);
+ if (eipServiceJson.has(ERRORS)) {
+ eventSender.setErrorResult(result, eipServiceJsonString);
+ } else {
+ provider.setEipServiceJson(eipServiceJson);
+ provider.setLastEipServiceUpdate(System.currentTimeMillis());
+ result.putBoolean(BROADCAST_RESULT_KEY, true);
+ }
+ } catch (NullPointerException | JSONException e) {
+ eventSender.setErrorResult(result, R.string.error_json_exception_user_message, null);
+ }
+ return result;
+ }
+
+ /**
+ * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate.
+ *
+ * @return true if certificate was downloaded correctly, false if provider.json is not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error.
+ */
+ protected Bundle updateVpnCertificate(Provider provider) {
+ Bundle result = new Bundle();
+ String certString = downloadFromVersionedApiUrlWithProviderCA("/" + PROVIDER_VPN_CERTIFICATE, provider);
+ if (DEBUG_MODE) {
+ VpnStatus.logDebug("[API] VPN CERT: " + certString);
+ }
+ if (ConfigHelper.checkErroneousDownload(certString)) {
+ if (TorStatusObservable.isRunning()) {
+ eventSender.setErrorResult(result, downloading_vpn_certificate_failed, null);
+ } else if (certString == null || certString.isEmpty() ){
+ // probably 204
+ eventSender.setErrorResult(result, error_io_exception_user_message, null);
+ } else {
+ eventSender.setErrorResult(result, certString);
+ }
+ return result;
+ }
+ return loadCertificate(provider, certString);
+ }
+
+ private Bundle loadCertificate(Provider provider, String certString) {
+ Bundle result = new Bundle();
+ if (certString == null) {
+ eventSender.setErrorResult(result, vpn_certificate_is_invalid, null);
+ return result;
+ }
+
+ try {
+ // API returns concatenated cert & key. Split them for OpenVPN options
+ String certificateString = null, keyString = null;
+ String[] certAndKey = certString.split("(?<=-\n)");
+
+ for (int i = 0; i < certAndKey.length - 1; i++) {
+ if (certAndKey[i].contains("KEY")) {
+ keyString = certAndKey[i++] + certAndKey[i];
+ } else if (certAndKey[i].contains("CERTIFICATE")) {
+ certificateString = certAndKey[i++] + certAndKey[i];
+ }
+ }
+
+ PrivateKey key = parsePrivateKeyFromString(keyString);
+ provider.setPrivateKeyString(getPEMFormattedPrivateKey(key));
+
+ ArrayList<X509Certificate> certificates = ConfigHelper.parseX509CertificatesFromString(certificateString);
+ certificates.get(0).checkValidity();
+ certificateString = Base64.encodeToString(certificates.get(0).getEncoded(), Base64.DEFAULT);
+ provider.setVpnCertificate( "-----BEGIN CERTIFICATE-----\n" + certificateString + "-----END CERTIFICATE-----");
+ result.putBoolean(BROADCAST_RESULT_KEY, true);
+ } catch (CertificateException | NullPointerException e) {
+ e.printStackTrace();
+ eventSender.setErrorResult(result, vpn_certificate_is_invalid, null);
+ }
+ return result;
+ }
+
+ /**
+ * Fetches the geo ip Json, containing a list of gateways sorted by distance from the users current location.
+ * Fetching is only allowed if the cache timeout of 1 h was reached, a valid geoip service URL exists and the
+ * vpn or tor is not running. The latter condition is needed in order to guarantee that the geoip service sees
+ * the real ip of the client
+ *
+ * @param provider
+ * @return
+ */
+ private Bundle getGeoIPJson(Provider provider) {
+ Bundle result = new Bundle();
+
+ if (!provider.shouldUpdateGeoIpJson() || provider.getGeoipUrl().isEmpty() || VpnStatus.isVPNActive() || TorStatusObservable.getStatus() != OFF) {
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+ return result;
+ }
+
+ try {
+ URL geoIpUrl = new URL(provider.getGeoipUrl());
+
+ String geoipJsonString = downloadFromUrlWithProviderCA(geoIpUrl.toString(), provider, false);
+ if (DEBUG_MODE) {
+ VpnStatus.logDebug("[API] MENSHEN JSON: " + geoipJsonString);
+ }
+ JSONObject geoipJson = new JSONObject(geoipJsonString);
+
+ if (geoipJson.has(ERRORS)) {
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+ } else{
+ provider.setGeoIpJson(geoipJson);
+ provider.setLastGeoIpUpdate(System.currentTimeMillis());
+ result.putBoolean(BROADCAST_RESULT_KEY, true);
+ }
+
+ } catch (JSONException | NullPointerException | MalformedURLException e) {
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+ e.printStackTrace();
+ }
+ return result;
+ }
+
+
+ private Bundle downloadCACert(Provider provider) {
+ Bundle result = new Bundle();
+ try {
+ String caCertUrl = provider.getDefinition().getString(Provider.CA_CERT_URI);
+ String providerDomain = provider.getDomain();
+ String certString = downloadWithCommercialCA(caCertUrl, provider);
+
+ if (validCertificate(provider, certString)) {
+ provider.setCaCert(certString);
+ if (DEBUG_MODE) {
+ VpnStatus.logDebug("[API] CA CERT: " + certString);
+ }
+ PreferenceHelper.putProviderString(providerDomain, Provider.CA_CERT, certString);
+ result.putBoolean(BROADCAST_RESULT_KEY, true);
+ } else {
+ eventSender.setErrorResult(result, warning_corrupted_provider_cert, ERROR_CERTIFICATE_PINNING.toString());
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+ return result;
+ }
+
+ private String downloadWithCommercialCA(String stringUrl, Provider provider) {
+ return downloadWithCommercialCA(stringUrl, provider, true);
+ }
+
+ /**
+ * Tries to download the contents of the provided url using commercially validated CA certificate from chosen provider.
+ *
+ */
+ private String downloadWithCommercialCA(String stringUrl, Provider provider, boolean allowRetry) {
+
+ String responseString;
+ JSONObject errorJson = new JSONObject();
+
+ OkHttpClient okHttpClient = clientGenerator.initCommercialCAHttpClient(errorJson, getProxyPort());
+ if (okHttpClient == null) {
+ return errorJson.toString();
+ }
+
+ List<Pair<String, String>> headerArgs = getAuthorizationHeader();
+
+ responseString = sendGetStringToServer(stringUrl, headerArgs, okHttpClient);
+
+ if (responseString != null && responseString.contains(ERRORS)) {
+ try {
+ // try to download with provider CA on certificate error
+ JSONObject responseErrorJson = new JSONObject(responseString);
+ if (responseErrorJson.getString(ERRORS).equals(getProviderFormattedString(resources, R.string.certificate_error))) {
+ responseString = downloadWithProviderCA(provider.getCaCert(), stringUrl);
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ try {
+ if (allowRetry &&
+ responseString != null &&
+ responseString.contains(ERRORS) &&
+ TorStatusObservable.getStatus() == OFF &&
+ torHandler.startTorProxy()
+ ) {
+ return downloadWithCommercialCA(stringUrl, provider, false);
+ }
+ } catch (InterruptedException | IllegalStateException | TimeoutException e) {
+ e.printStackTrace();
+ }
+ return responseString;
+ }
+
+
+ /**
+ * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider.
+ *
+ * @return an empty string if it fails, the response body if not.
+ */
+ private String downloadFromApiUrlWithProviderCA(String path, Provider provider) {
+ String baseUrl = provider.getApiUrl();
+ String urlString = baseUrl + path;
+ return downloadFromUrlWithProviderCA(urlString, provider);
+ }
+
+ /**
+ * Tries to download the contents of $base_url/$version/$path using not commercially validated CA certificate from chosen provider.
+ *
+ * @return an empty string if it fails, the response body if not.
+ */
+ private String downloadFromVersionedApiUrlWithProviderCA(String path, Provider provider) {
+ String baseUrl = provider.getApiUrlWithVersion();
+ String urlString = baseUrl + path;
+ return downloadFromUrlWithProviderCA(urlString, provider);
+ }
+
+ private String downloadFromUrlWithProviderCA(String urlString, Provider provider) {
+ return downloadFromUrlWithProviderCA(urlString, provider, true);
+ }
+
+ private String downloadFromUrlWithProviderCA(String urlString, Provider provider, boolean allowRetry) {
+ String responseString;
+ JSONObject errorJson = new JSONObject();
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), getProxyPort(), errorJson);
+ if (okHttpClient == null) {
+ return errorJson.toString();
+ }
+
+ List<Pair<String, String>> headerArgs = getAuthorizationHeader();
+ responseString = sendGetStringToServer(urlString, headerArgs, okHttpClient);
+
+ try {
+ if (allowRetry &&
+ responseString != null &&
+ responseString.contains(ERRORS) &&
+ TorStatusObservable.getStatus() == OFF &&
+ torHandler.startTorProxy()
+ ) {
+ return downloadFromUrlWithProviderCA(urlString, provider, false);
+ }
+ } catch (InterruptedException | IllegalStateException | TimeoutException e) {
+ e.printStackTrace();
+ }
+
+ return responseString;
+ }
+
+
+ /**
+ * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider.
+ *
+ * @param urlString as a string
+ * @return an empty string if it fails, the url content if not.
+ */
+ private String downloadWithProviderCA(String caCert, String urlString) {
+ JSONObject initError = new JSONObject();
+ String responseString;
+
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(caCert, getProxyPort(), initError);
+ if (okHttpClient == null) {
+ return initError.toString();
+ }
+
+ List<Pair<String, String>> headerArgs = getAuthorizationHeader();
+
+ responseString = sendGetStringToServer(urlString, headerArgs, okHttpClient);
+
+ return responseString;
+ }
+
+ protected String sendGetStringToServer(@NonNull String url, @NonNull List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) {
+ return requestStringFromServer(url, "GET", null, headerArgs, okHttpClient);
+ }
+
+ private String requestStringFromServer(@NonNull String url, @NonNull String requestMethod, String jsonString, @NonNull List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) {
+ String plainResponseBody;
+
+ try {
+
+ plainResponseBody = ProviderApiConnector.requestStringFromServer(url, requestMethod, jsonString, headerArgs, okHttpClient);
+
+ } catch (NullPointerException npe) {
+ plainResponseBody = eventSender.formatErrorMessage(error_json_exception_user_message);
+ VpnStatus.logWarning("[API] Null response body for request " + url + ": " + npe.getLocalizedMessage());
+ } catch (UnknownHostException | SocketTimeoutException e) {
+ plainResponseBody = eventSender.formatErrorMessage(server_unreachable_message);
+ VpnStatus.logWarning("[API] UnknownHostException or SocketTimeoutException for request " + url + ": " + e.getLocalizedMessage());
+ } catch (MalformedURLException e) {
+ plainResponseBody = eventSender.formatErrorMessage(malformed_url);
+ VpnStatus.logWarning("[API] MalformedURLException for request " + url + ": " + e.getLocalizedMessage());
+ } catch (SSLHandshakeException | SSLPeerUnverifiedException e) {
+ plainResponseBody = eventSender.formatErrorMessage(certificate_error);
+ VpnStatus.logWarning("[API] SSLHandshakeException or SSLPeerUnverifiedException for request " + url + ": " + e.getLocalizedMessage());
+ } catch (ConnectException e) {
+ plainResponseBody = eventSender.formatErrorMessage(service_is_down_error);
+ VpnStatus.logWarning("[API] ConnectException for request " + url + ": " + e.getLocalizedMessage());
+ } catch (IllegalArgumentException e) {
+ plainResponseBody = eventSender.formatErrorMessage(error_no_such_algorithm_exception_user_message);
+ VpnStatus.logWarning("[API] IllegalArgumentException for request " + url + ": " + e.getLocalizedMessage());
+ } catch (UnknownServiceException e) {
+ //unable to find acceptable protocols - tlsv1.2 not enabled?
+ plainResponseBody = eventSender.formatErrorMessage(error_no_such_algorithm_exception_user_message);
+ VpnStatus.logWarning("[API] UnknownServiceException for request " + url + ": " + e.getLocalizedMessage());
+ } catch (IOException e) {
+ plainResponseBody = eventSender.formatErrorMessage(error_io_exception_user_message);
+ VpnStatus.logWarning("[API] IOException for request " + url + ": " + e.getLocalizedMessage());
+ }
+
+ return plainResponseBody;
+ }
+
+ private boolean canConnect(Provider provider, Bundle result) {
+ return canConnect(provider, result, 0);
+ }
+
+ private boolean canConnect(Provider provider, Bundle result, int tries) {
+ JSONObject errorJson = new JSONObject();
+ String providerUrl = provider.getApiUrl() + "/provider.json";
+
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), getProxyPort(), errorJson);
+ if (okHttpClient == null) {
+ result.putString(ERRORS, errorJson.toString());
+ return false;
+ }
+
+ if (tries > 0) {
+ result.remove(ERRORS);
+ }
+
+ try {
+ return ProviderApiConnector.canConnect(okHttpClient, providerUrl);
+
+ } catch (UnknownHostException | SocketTimeoutException e) {
+ VpnStatus.logWarning("[API] UnknownHostException or SocketTimeoutException during connection check: " + e.getLocalizedMessage());
+ eventSender.setErrorResult(result, server_unreachable_message, null);
+ } catch (MalformedURLException e) {
+ VpnStatus.logWarning("[API] MalformedURLException during connection check: " + e.getLocalizedMessage());
+ eventSender.setErrorResult(result, malformed_url, null);
+ } catch (SSLHandshakeException e) {
+ VpnStatus.logWarning("[API] SSLHandshakeException during connection check: " + e.getLocalizedMessage());
+ eventSender.setErrorResult(result, warning_corrupted_provider_cert, ERROR_INVALID_CERTIFICATE.toString());
+ } catch (ConnectException e) {
+ VpnStatus.logWarning("[API] ConnectException during connection check: " + e.getLocalizedMessage());
+ eventSender.setErrorResult(result, service_is_down_error, null);
+ } catch (IllegalArgumentException e) {
+ VpnStatus.logWarning("[API] IllegalArgumentException during connection check: " + e.getLocalizedMessage());
+ eventSender.setErrorResult(result, error_no_such_algorithm_exception_user_message, null);
+ } catch (UnknownServiceException e) {
+ VpnStatus.logWarning("[API] UnknownServiceException during connection check: " + e.getLocalizedMessage());
+ //unable to find acceptable protocols - tlsv1.2 not enabled?
+ eventSender.setErrorResult(result, error_no_such_algorithm_exception_user_message, null);
+ } catch (IOException e) {
+ VpnStatus.logWarning("[API] IOException during connection check: " + e.getLocalizedMessage());
+ eventSender.setErrorResult(result, error_io_exception_user_message, null);
+ }
+
+ try {
+ if (tries == 0 &&
+ result.containsKey(ERRORS) &&
+ TorStatusObservable.getStatus() == OFF &&
+ torHandler.startTorProxy()
+ ) {
+ return canConnect(provider, result, 1);
+ }
+ } catch (InterruptedException | IllegalStateException | TimeoutException e) {
+ e.printStackTrace();
+ }
+
+ return false;
+ }
+
+ Bundle validateProviderDetails(Provider provider) {
+ Bundle result = new Bundle();
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+
+ if (!provider.hasDefinition()) {
+ return result;
+ }
+
+ result = validateCertificateForProvider(result, provider);
+
+ //invalid certificate or no certificate or unable to connect due other connectivity issues
+ if (result.containsKey(ERRORS) || (result.containsKey(BROADCAST_RESULT_KEY) && !result.getBoolean(BROADCAST_RESULT_KEY)) ) {
+ return result;
+ }
+
+ result.putBoolean(BROADCAST_RESULT_KEY, true);
+
+ return result;
+ }
+
+ protected Bundle validateCertificateForProvider(Bundle result, Provider provider) {
+ String caCert = provider.getCaCert();
+
+ if (ConfigHelper.checkErroneousDownload(caCert)) {
+ VpnStatus.logWarning("[API] No provider cert.");
+ return result;
+ }
+
+ ArrayList<X509Certificate> certificates = ConfigHelper.parseX509CertificatesFromString(caCert);
+ if (certificates == null) {
+ return eventSender.setErrorResult(result, warning_corrupted_provider_cert, ERROR_INVALID_CERTIFICATE.toString());
+ }
+ try {
+ String encoding = provider.getCertificatePinEncoding();
+ String expectedFingerprint = provider.getCertificatePin();
+
+ // Do certificate pinning only if we have 1 cert, otherwise we assume some transitioning of
+ // X509 certs, therefore we cannot do cert pinning
+ if (certificates.size() == 1) {
+ String realFingerprint = getFingerprintFromCertificate(certificates.get(0), encoding);
+ if (!realFingerprint.trim().equalsIgnoreCase(expectedFingerprint.trim())) {
+ return eventSender.setErrorResult(result, warning_corrupted_provider_cert, ERROR_CERTIFICATE_PINNING.toString());
+ }
+ }
+ for (X509Certificate certificate : certificates) {
+ certificate.checkValidity();
+ }
+
+ if (!canConnect(provider, result)) {
+ return result;
+ }
+ } catch (NoSuchAlgorithmException e ) {
+ return eventSender.setErrorResult(result, error_no_such_algorithm_exception_user_message, null);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ return eventSender.setErrorResult(result, warning_corrupted_provider_details, ERROR_CORRUPTED_PROVIDER_JSON.toString());
+ } catch (CertificateEncodingException | CertificateNotYetValidException |
+ CertificateExpiredException e) {
+ return eventSender.setErrorResult(result, warning_expired_provider_cert, ERROR_INVALID_CERTIFICATE.toString());
+ }
+
+ result.putBoolean(BROADCAST_RESULT_KEY, true);
+ return result;
+ }
+
+ @NonNull
+ protected List<Pair<String, String>> getAuthorizationHeader() {
+ List<Pair<String, String>> headerArgs = new ArrayList<>();
+ if (!LeapSRPSession.getToken().isEmpty()) {
+ Pair<String, String> authorizationHeaderPair = new Pair<>(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken());
+ headerArgs.add(authorizationHeaderPair);
+ }
+ return headerArgs;
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerV5.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerV5.java
new file mode 100644
index 00000000..2680f612
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerV5.java
@@ -0,0 +1,354 @@
+package se.leap.bitmaskclient.providersetup;
+
+import static android.text.TextUtils.isEmpty;
+import static se.leap.bitmaskclient.R.string.malformed_url;
+import static se.leap.bitmaskclient.R.string.vpn_certificate_is_invalid;
+import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_cert;
+import static se.leap.bitmaskclient.R.string.warning_expired_provider_cert;
+import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.COUNTRYCODE;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_EIP_SERVICE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_SERVICE_JSON;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.PROVIDER_NOK;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.PROVIDER_OK;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.QUIETLY_UPDATE_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.SET_UP_PROVIDER;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_INVALID_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupObservable.DOWNLOADED_V5_BRIDGES;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupObservable.DOWNLOADED_V5_GATEWAYS;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupObservable.DOWNLOADED_V5_SERVICE_JSON;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupObservable.DOWNLOADED_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus.OFF;
+
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+
+import de.blinkt.openvpn.core.VpnStatus;
+import mobile.BitmaskMobile;
+import se.leap.bitmaskclient.BuildConfig;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.base.models.ProviderObservable;
+import se.leap.bitmaskclient.base.utils.ConfigHelper;
+import se.leap.bitmaskclient.base.utils.CredentialsParser;
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import se.leap.bitmaskclient.eip.EipStatus;
+import se.leap.bitmaskclient.tor.TorStatusObservable;
+
+public class ProviderApiManagerV5 extends ProviderApiManagerBase implements IProviderApiManager {
+
+ private static final String TAG = ProviderApiManagerV5.class.getSimpleName();
+
+ ProviderApiManagerV5(Resources resources, ProviderApiServiceCallback callback) {
+ super(resources, callback);
+ }
+
+ @Override
+ public void handleAction(String action, Provider provider, Bundle parameters, ResultReceiver receiver) {
+ Bundle result = new Bundle();
+ switch (action) {
+ case SET_UP_PROVIDER:
+ result = setupProvider(provider, parameters);
+ if (result.getBoolean(BROADCAST_RESULT_KEY)) {
+ serviceCallback.saveProvider(provider);
+ eventSender.sendToReceiverOrBroadcast(receiver, PROVIDER_OK, result, provider);
+ } else {
+ eventSender.sendToReceiverOrBroadcast(receiver, PROVIDER_NOK, result, provider);
+ }
+ break;
+
+ case DOWNLOAD_SERVICE_JSON:
+ result = updateServiceInfos(provider, parameters);
+ if (result.getBoolean(BROADCAST_RESULT_KEY)) {
+ serviceCallback.saveProvider(provider);
+ eventSender.sendToReceiverOrBroadcast(receiver, CORRECTLY_DOWNLOADED_EIP_SERVICE, result, provider);
+ } else {
+ eventSender.sendToReceiverOrBroadcast(receiver, INCORRECTLY_DOWNLOADED_EIP_SERVICE, result, provider);
+ }
+ break;
+
+ case QUIETLY_UPDATE_VPN_CERTIFICATE:
+ result = updateVpnCertificate(provider);
+ if (result.getBoolean(BROADCAST_RESULT_KEY)) {
+ Log.d(TAG, "successfully downloaded VPN certificate");
+ provider.setShouldUpdateVpnCertificate(false);
+ PreferenceHelper.storeProviderInPreferences(provider);
+ ProviderObservable.getInstance().updateProvider(provider);
+ }
+ break;
+ case UPDATE_INVALID_VPN_CERTIFICATE:
+ result = updateVpnCertificate(provider);
+ if (result.getBoolean(BROADCAST_RESULT_KEY)) {
+ eventSender.sendToReceiverOrBroadcast(receiver, CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE, result, provider);
+ } else {
+ eventSender.sendToReceiverOrBroadcast(receiver, INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE, result, provider);
+ }
+ break;
+ }
+
+ }
+
+ private Bundle updateServiceInfos(Provider provider, Bundle parameters) {
+ Bundle currentDownload = new Bundle();
+
+ BitmaskMobile bm;
+ try {
+ bm = new BitmaskMobile(provider.getMainUrl(), new PreferenceHelper.SharedPreferenceStore());
+ bm.setDebug(BuildConfig.DEBUG);
+ } catch (IllegalStateException e) {
+ return eventSender.setErrorResult(currentDownload, R.string.config_error_found, null);
+ }
+
+ try {
+ configureBaseCountryCode(bm, parameters);
+ } catch (Exception e) {
+ return eventSender.setErrorResult(currentDownload, R.string.config_error_found, null);
+ }
+
+ try {
+ String serviceJson = bm.getService();
+ provider.setService(serviceJson);
+ } catch (Exception e) {
+ return eventSender.setErrorResult(currentDownload, R.string.config_error_found, null);
+ }
+
+ try {
+ if (provider.hasIntroducer()) {
+ bm.setIntroducer(provider.getIntroducer().toUrl());
+ }
+ } catch (Exception e) {
+ return eventSender.setErrorResult(currentDownload, R.string.config_error_found, null);
+ }
+
+ if (PreferenceHelper.getUseBridges()) {
+ try {
+ String bridgesJson = bm.getAllBridges("", "", "", "");
+ provider.setBridges(bridgesJson);
+ } catch (Exception e) {
+ // TODO: send failed to fetch bridges event
+ return eventSender.setErrorResult(currentDownload, R.string.config_error_found, null);
+ }
+ } else {
+ try {
+ String gatewaysJson = bm.getAllGateways("", "", "");
+ provider.setGateways(gatewaysJson);
+ } catch (Exception e) {
+ // TODO: send
+ return eventSender.setErrorResult(currentDownload, R.string.config_error_found, null);
+
+ }
+ }
+ currentDownload.putBoolean(BROADCAST_RESULT_KEY, true);
+ return currentDownload;
+
+ }
+
+ protected Bundle setupProvider(Provider provider, Bundle parameters) {
+ Bundle currentDownload = new Bundle();
+
+ if (isEmpty(provider.getMainUrl()) || provider.getMainUrl().isEmpty()) {
+ currentDownload.putBoolean(BROADCAST_RESULT_KEY, false);
+ eventSender.setErrorResult(currentDownload, malformed_url, null);
+ VpnStatus.logWarning("[API] MainURL String is not set. Cannot setup provider.");
+ return currentDownload;
+ }
+
+ //provider certificate invalid
+ if (currentDownload.containsKey(ERRORS)) {
+ currentDownload.putParcelable(PROVIDER_KEY, provider);
+ return currentDownload;
+ }
+
+ BitmaskMobile bm;
+ try {
+ bm = new BitmaskMobile(provider.getMainUrl(), new PreferenceHelper.SharedPreferenceStore());
+ bm.setDebug(BuildConfig.DEBUG);
+ if (TorStatusObservable.isRunning() && TorStatusObservable.getSocksProxyPort() != -1) {
+ bm.setSocksProxy(SOCKS_PROXY_SCHEME + PROXY_HOST + ":" + TorStatusObservable.getSocksProxyPort());
+ }
+ if (provider.hasIntroducer()) {
+ bm.setIntroducer(provider.getIntroducer().toUrl());
+ }
+ } catch (Exception e) {
+ // TODO: improve error message
+ return eventSender.setErrorResult(currentDownload, R.string.config_error_found, null);
+ }
+
+ try {
+ configureBaseCountryCode(bm, parameters);
+ } catch (Exception e) {
+ return eventSender.setErrorResult(currentDownload, R.string.config_error_found, null);
+ }
+
+ try {
+ String serviceJson = bm.getService();
+ Log.d(TAG, "service Json reponse: " + serviceJson);
+ provider.setService(serviceJson);
+ ProviderSetupObservable.updateProgress(DOWNLOADED_V5_SERVICE_JSON);
+ } catch (Exception e) {
+ Log.w(TAG, "failed to fetch service.json: " + e.getMessage());
+ e.printStackTrace();
+ return eventSender.setErrorResult(currentDownload, R.string.error_json_exception_user_message, null);
+ }
+
+ try {
+ String gatewaysJson = bm.getAllGateways("", "", "");
+ Log.d(TAG, "gateways Json reponse: " + gatewaysJson);
+ provider.setGateways(gatewaysJson);
+ ProviderSetupObservable.updateProgress(DOWNLOADED_V5_GATEWAYS);
+ } catch (Exception e) {
+ Log.w(TAG, "failed to fetch gateways: " + e.getMessage());
+ e.printStackTrace();
+ return eventSender.setErrorResult(currentDownload, R.string.error_json_exception_user_message, null);
+ }
+
+ try {
+ String bridgesJson = bm.getAllBridges("", "", "", "");
+ Log.d(TAG, "bridges Json reponse: " + bridgesJson);
+ provider.setBridges(bridgesJson);
+ ProviderSetupObservable.updateProgress(DOWNLOADED_V5_BRIDGES);
+ } catch (Exception e) {
+ Log.w(TAG, "failed to fetch bridges: " + e.getMessage());
+ e.printStackTrace();
+ return eventSender.setErrorResult(currentDownload, R.string.error_json_exception_user_message, null);
+ }
+
+ try {
+ String cert = bm.getOpenVPNCert();
+ currentDownload = loadCredentials(provider, cert);
+ currentDownload = validateCertificateForProvider(currentDownload, provider);
+ ProviderSetupObservable.updateProgress(DOWNLOADED_VPN_CERTIFICATE);
+ } catch (Exception e) {
+ return eventSender.setErrorResult(currentDownload, R.string.error_json_exception_user_message, null);
+ }
+
+ return currentDownload;
+ }
+
+ private Bundle loadCredentials(Provider provider, String credentials) {
+ Bundle result = new Bundle();
+
+ try {
+ CredentialsParser.parseXml(credentials, provider);
+ } catch (XmlPullParserException | IOException e) {
+ e.printStackTrace();
+ return eventSender.setErrorResult(result, vpn_certificate_is_invalid, null);
+ }
+
+ result.putBoolean(BROADCAST_RESULT_KEY, true);
+ return result;
+ }
+
+ @Nullable
+ private void configureBaseCountryCode(BitmaskMobile bm, Bundle parameters) throws Exception {
+ String cc = parameters.getString(COUNTRYCODE, null);
+ if (cc == null &&
+ EipStatus.getInstance().isDisconnected() &&
+ TorStatusObservable.getStatus() == OFF) {
+ try {
+ cc = bm.getGeolocation();
+ } catch (Exception e) {
+ // print exception and ignore
+ e.printStackTrace();
+ cc = "";
+ }
+ }
+ bm.setCountryCode(cc);
+ }
+
+ Bundle validateProviderDetails(Provider provider) {
+ Bundle result = new Bundle();
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+
+ if (!provider.hasDefinition()) {
+ return result;
+ }
+
+ result = validateCertificateForProvider(result, provider);
+
+ //invalid certificate or no certificate or unable to connect due other connectivity issues
+ if (result.containsKey(ERRORS) || (result.containsKey(BROADCAST_RESULT_KEY) && !result.getBoolean(BROADCAST_RESULT_KEY)) ) {
+ return result;
+ }
+
+ result.putBoolean(BROADCAST_RESULT_KEY, true);
+
+ return result;
+ }
+
+ protected Bundle validateCertificateForProvider(Bundle result, Provider provider) {
+ String caCert = provider.getCaCert();
+
+ if (ConfigHelper.checkErroneousDownload(caCert)) {
+ VpnStatus.logWarning("[API] No provider cert.");
+ return result;
+ }
+
+ ArrayList<X509Certificate> certificates = ConfigHelper.parseX509CertificatesFromString(caCert);
+ if (certificates == null) {
+ return eventSender.setErrorResult(result, warning_corrupted_provider_cert, ERROR_INVALID_CERTIFICATE.toString());
+ }
+
+ ArrayList<X509Certificate> validCertificates = new ArrayList<>();
+ int invalidCertificates = 0;
+ for (X509Certificate certificate : certificates) {
+ try {
+ certificate.checkValidity();
+ validCertificates.add(certificate);
+ } catch (CertificateNotYetValidException |
+ CertificateExpiredException e) {
+ e.printStackTrace();
+ invalidCertificates++;
+ }
+ }
+ if (validCertificates.isEmpty() && invalidCertificates > 0) {
+ return eventSender.setErrorResult(result, warning_expired_provider_cert, ERROR_INVALID_CERTIFICATE.toString());
+ }
+
+ provider.setCaCert(ConfigHelper.parseX509CertificatesToString(validCertificates));
+ result.putParcelable(PROVIDER_KEY, provider);
+ result.putBoolean(BROADCAST_RESULT_KEY, true);
+ return result;
+ }
+
+ protected Bundle updateVpnCertificate(Provider provider) {
+ Bundle currentDownload = new Bundle();
+ BitmaskMobile bm;
+ try {
+ bm = new BitmaskMobile(provider.getMainUrl(), new PreferenceHelper.SharedPreferenceStore());
+ bm.setDebug(BuildConfig.DEBUG);
+ } catch (IllegalStateException e) {
+ return eventSender.setErrorResult(currentDownload, R.string.config_error_found, null);
+ }
+
+ try {
+ String cert = bm.getOpenVPNCert();
+ currentDownload = loadCredentials(provider, cert);
+ currentDownload = validateCertificateForProvider(currentDownload, provider);
+ ProviderSetupObservable.updateProgress(DOWNLOADED_VPN_CERTIFICATE);
+ } catch (Exception e) {
+ return eventSender.setErrorResult(currentDownload, R.string.error_json_exception_user_message, null);
+ }
+
+ return currentDownload;
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiSetupBroadcastReceiver.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiSetupBroadcastReceiver.java
index ee39499b..4b8e8e18 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiSetupBroadcastReceiver.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiSetupBroadcastReceiver.java
@@ -18,12 +18,16 @@ package se.leap.bitmaskclient.providersetup;
import static android.app.Activity.RESULT_CANCELED;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
+import androidx.core.os.BundleCompat;
+
import java.lang.ref.WeakReference;
import se.leap.bitmaskclient.base.models.Constants;
@@ -61,10 +65,10 @@ public class ProviderApiSetupBroadcastReceiver extends BroadcastReceiver {
Log.d(TAG, "Broadcast resultCode: " + resultCode);
Bundle resultData = intent.getParcelableExtra(Constants.BROADCAST_RESULT_KEY);
- Provider handledProvider = resultData.getParcelable(Constants.PROVIDER_KEY);
+ Provider handledProvider = resultData == null ? null : BundleCompat.getParcelable(resultData, PROVIDER_KEY, Provider.class);
if (handledProvider != null && setupInterface.getProvider() != null &&
- handledProvider.getMainUrlString().equalsIgnoreCase(setupInterface.getProvider().getMainUrlString())) {
+ handledProvider.getMainUrl().equalsIgnoreCase(setupInterface.getProvider().getMainUrl())) {
switch (resultCode) {
case ProviderAPI.PROVIDER_OK:
setupInterface.handleProviderSetUp(handledProvider);
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiTorHandler.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiTorHandler.java
new file mode 100644
index 00000000..3b7d4247
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiTorHandler.java
@@ -0,0 +1,54 @@
+package se.leap.bitmaskclient.providersetup;
+
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.getTorTimeout;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus.ON;
+
+import org.jetbrains.annotations.Blocking;
+
+import java.util.concurrent.TimeoutException;
+
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import se.leap.bitmaskclient.eip.EipStatus;
+import se.leap.bitmaskclient.tor.TorStatusObservable;
+
+public class ProviderApiTorHandler {
+
+ ProviderApiManagerBase.ProviderApiServiceCallback serviceCallback;
+ public ProviderApiTorHandler(ProviderApiManagerBase.ProviderApiServiceCallback callback) {
+ this.serviceCallback = callback;
+ }
+
+ @Blocking
+ public boolean startTorProxy() throws InterruptedException, IllegalStateException, TimeoutException {
+ if (EipStatus.getInstance().isDisconnected() &&
+ PreferenceHelper.getUseSnowflake() &&
+ serviceCallback.startTorService()) {
+ waitForTorCircuits();
+ if (TorStatusObservable.isCancelled()) {
+ throw new InterruptedException("Cancelled Tor setup.");
+ }
+ int port = serviceCallback.getTorHttpTunnelPort();
+ TorStatusObservable.setProxyPort(port);
+ int socksPort = serviceCallback.getTorSocksProxyPort();
+ TorStatusObservable.setSocksProxyPort(socksPort);
+ return port != -1 && socksPort != -1;
+ }
+ return false;
+ }
+
+ public void stopTorProxy() {
+ serviceCallback.stopTorService();
+ }
+
+ private void waitForTorCircuits() throws InterruptedException, TimeoutException {
+ if (TorStatusObservable.getStatus() == ON) {
+ return;
+ }
+ TorStatusObservable.waitUntil(this::isTorOnOrCancelled, getTorTimeout());
+ }
+
+ private boolean isTorOnOrCancelled() {
+ return TorStatusObservable.getStatus() == ON || TorStatusObservable.isCancelled();
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java
index 9eacae5d..bcb177e2 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java
@@ -45,8 +45,11 @@ public class ProviderManager {
private boolean addDummyEntry = false;
public static ProviderManager getInstance(AssetManager assetsManager) {
- if (instance == null)
+ if (instance == null) {
instance = new ProviderManager(assetsManager);
+ } else {
+ instance.updateCustomProviders();
+ }
return instance;
}
@@ -63,7 +66,7 @@ public class ProviderManager {
private ProviderManager(AssetManager assetManager) {
this.assetsManager = assetManager;
addDefaultProviders(assetManager);
- addCustomProviders();
+ updateCustomProviders();
}
private void addDefaultProviders(AssetManager assetManager) {
@@ -78,7 +81,7 @@ public class ProviderManager {
private Set<String> getProviderUrlSetFromProviderSet(Set<Provider> providers) {
HashSet<String> providerUrls = new HashSet<>();
for (Provider provider : providers) {
- providerUrls.add(provider.getMainUrl().toString());
+ providerUrls.add(provider.getMainUrl());
}
return providerUrls;
}
@@ -117,7 +120,7 @@ public class ProviderManager {
}
- private void addCustomProviders() {
+ public void updateCustomProviders() {
customProviders = PreferenceHelper.getCustomProviders();
}
@@ -152,10 +155,10 @@ public class ProviderManager {
public boolean add(Provider element) {
boolean addElement = element != null &&
- !defaultProviderURLs.contains(element.getMainUrlString()) &&
- !customProviders.containsKey(element.getMainUrlString());
+ !defaultProviderURLs.contains(element.getMainUrl()) &&
+ !customProviders.containsKey(element.getMainUrl());
if (addElement) {
- customProviders.put(element.getMainUrlString(), element);
+ customProviders.put(element.getMainUrl(), element);
return true;
}
return false;
@@ -163,7 +166,7 @@ public class ProviderManager {
public boolean remove(Object element) {
return element instanceof Provider &&
- customProviders.remove(((Provider) element).getMainUrlString()) != null;
+ customProviders.remove(((Provider) element).getMainUrl()) != null;
}
public boolean addAll(Collection<? extends Provider> elements) {
@@ -171,9 +174,9 @@ public class ProviderManager {
boolean addedAll = true;
while (iterator.hasNext()) {
Provider p = (Provider) iterator.next();
- boolean containsKey = customProviders.containsKey(p.getMainUrlString());
+ boolean containsKey = customProviders.containsKey(p.getMainUrl());
if (!containsKey) {
- customProviders.put(p.getMainUrlString(), p);
+ customProviders.put(p.getMainUrl(), p);
}
addedAll = !containsKey && addedAll;
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java
index 338a60e9..172d2636 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java
@@ -24,6 +24,7 @@ import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.os.BundleCompat;
import androidx.fragment.app.DialogFragment;
import org.json.JSONObject;
@@ -32,6 +33,7 @@ import se.leap.bitmaskclient.R;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORID;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.INITIAL_ACTION;
@@ -136,7 +138,7 @@ public class ProviderSetupFailedDialog extends DialogFragment {
break;
case ERROR_NEW_URL_NO_VPN_PROVIDER:
builder.setPositiveButton(R.string.retry, (dialog, id)
- -> interfaceWithConfigurationWizard.addAndSelectNewProvider(provider.getMainUrlString()));
+ -> interfaceWithConfigurationWizard.addAndSelectNewProvider(provider.getMainUrl()));
break;
case ERROR_TOR_TIMEOUT:
builder.setPositiveButton(R.string.retry, (dialog, id) -> {
@@ -219,7 +221,7 @@ public class ProviderSetupFailedDialog extends DialogFragment {
return;
}
if (savedInstanceState.containsKey(KEY_PROVIDER)) {
- this.provider = savedInstanceState.getParcelable(KEY_PROVIDER);
+ this.provider = BundleCompat.getParcelable(savedInstanceState, KEY_PROVIDER, Provider.class);
}
if (savedInstanceState.containsKey(KEY_REASON_TO_FAIL)) {
this.reasonToFail = savedInstanceState.getString(KEY_REASON_TO_FAIL);
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupObservable.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupObservable.java
index c882b0bb..d57e1739 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupObservable.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupObservable.java
@@ -16,6 +16,8 @@ package se.leap.bitmaskclient.providersetup;
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+import android.os.Bundle;
+
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
@@ -36,6 +38,9 @@ public class ProviderSetupObservable {
private boolean canceled = false;
public static final int DOWNLOADED_PROVIDER_JSON = 20;
public static final int DOWNLOADED_CA_CERT = 40;
+ public static final int DOWNLOADED_V5_SERVICE_JSON = 40;
+ public static final int DOWNLOADED_V5_GATEWAYS = 60;
+ public static final int DOWNLOADED_V5_BRIDGES = 80;
public static final int DOWNLOADED_EIP_SERVICE_JSON = 60;
public static final int DOWNLOADED_GEOIP_JSON = 80;
public static final int DOWNLOADED_VPN_CERTIFICATE = 100;
@@ -45,13 +50,15 @@ public class ProviderSetupObservable {
public static final String PROPERTY_CHANGE = "ProviderSetupObservable";
private final HandlerInterface handler;
private long lastUpdate = 0;
+ private int resultCode = 0;
+ private Bundle resultData;
private ProviderSetupObservable() {
handler = HandlerProvider.get();
changeSupport = new PropertyChangeSupport(this);
-
+ resultData = new Bundle();
}
public void addObserver(PropertyChangeListener propertyChangeListener) {
@@ -70,6 +77,14 @@ public class ProviderSetupObservable {
return instance;
}
+ public static void storeLastResult(int resultCode, Bundle resultData) {
+ if (getInstance().canceled) {
+ return;
+ }
+ getInstance().resultCode = resultCode;
+ getInstance().resultData = resultData;
+ }
+
public static void updateProgress(int progress) {
if (getInstance().canceled) {
return;
@@ -105,8 +120,14 @@ public class ProviderSetupObservable {
return getInstance().progress;
}
+ public static boolean isSetupRunning() {
+ return getInstance().progress > 0;
+ }
+
public static void reset() {
getInstance().progress = 0;
+ getInstance().resultCode = 0;
+ getInstance().resultData = new Bundle();
getInstance().changeSupport.firePropertyChange(PROPERTY_CHANGE, null, getInstance());
}
@@ -121,5 +142,16 @@ public class ProviderSetupObservable {
public static void startSetup() {
getInstance().canceled = false;
+ getInstance().resultCode = 0;
+ getInstance().progress = 1;
+ getInstance().resultData = new Bundle();
+ }
+
+ public static int getResultCode() {
+ return getInstance().resultCode;
+ }
+
+ public static Bundle getResultData() {
+ return getInstance().resultData;
}
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/SetupViewPagerAdapter.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/SetupViewPagerAdapter.java
index fb190dc2..24b4179f 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/SetupViewPagerAdapter.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/SetupViewPagerAdapter.java
@@ -7,7 +7,6 @@ import static se.leap.bitmaskclient.providersetup.fragments.SetupFragmentFactory
import static se.leap.bitmaskclient.providersetup.fragments.SetupFragmentFactory.NOTIFICATION_PERMISSON_FRAGMENT;
import static se.leap.bitmaskclient.providersetup.fragments.SetupFragmentFactory.PROVIDER_SELECTION_FRAGMENT;
import static se.leap.bitmaskclient.providersetup.fragments.SetupFragmentFactory.SUCCESS_FRAGMENT;
-import static se.leap.bitmaskclient.providersetup.fragments.SetupFragmentFactory.VPN_PERMISSON_EDUCATIONAL_FRAGMENT;
import static se.leap.bitmaskclient.providersetup.fragments.SetupFragmentFactory.VPN_PERMISSON_FRAGMENT;
import android.content.Intent;
@@ -38,19 +37,21 @@ public class SetupViewPagerAdapter extends FragmentStateAdapter {
fragments.add(PROVIDER_SELECTION_FRAGMENT);
}
fragments.add(CIRCUMVENTION_SETUP_FRAGMENT);
- fragments.add(CONFIGURE_PROVIDER_FRAGMENT);
}
-
+ if (showNotificationPermission || vpnPermissionRequest != null) {
+ fragments.add(NOTIFICATION_PERMISSON_EDUCATIONAL_FRAGMENT);
+ }
if (vpnPermissionRequest != null) {
- fragments.add(VPN_PERMISSON_EDUCATIONAL_FRAGMENT);
fragments.add(VPN_PERMISSON_FRAGMENT);
}
if (showNotificationPermission) {
- fragments.add(NOTIFICATION_PERMISSON_EDUCATIONAL_FRAGMENT);
fragments.add(NOTIFICATION_PERMISSON_FRAGMENT);
}
+ if (providerSetup) {
+ fragments.add(CONFIGURE_PROVIDER_FRAGMENT);
+ }
fragments.add(SUCCESS_FRAGMENT);
- setupFragmentFactory = new SetupFragmentFactory(fragments, vpnPermissionRequest);
+ setupFragmentFactory = new SetupFragmentFactory(fragments, vpnPermissionRequest, showNotificationPermission);
}
@NonNull
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java
index 9235daad..191db42b 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java
@@ -3,9 +3,12 @@ package se.leap.bitmaskclient.providersetup.activities;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static androidx.appcompat.app.ActionBar.DISPLAY_SHOW_CUSTOM;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
import static se.leap.bitmaskclient.base.utils.BuildConfigHelper.isDefaultBitmask;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.deleteProviderDetailsFromPreferences;
+import static se.leap.bitmaskclient.providersetup.fragments.SetupFragmentFactory.CIRCUMVENTION_SETUP_FRAGMENT;
import static se.leap.bitmaskclient.providersetup.fragments.SetupFragmentFactory.CONFIGURE_PROVIDER_FRAGMENT;
+import static se.leap.bitmaskclient.providersetup.fragments.SetupFragmentFactory.PROVIDER_SELECTION_FRAGMENT;
import static se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus.OFF;
import android.Manifest;
@@ -29,8 +32,11 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.core.content.res.ResourcesCompat;
+import androidx.core.os.BundleCompat;
import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import org.json.JSONException;
@@ -38,10 +44,12 @@ import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashSet;
+import java.util.Objects;
import se.leap.bitmaskclient.BuildConfig;
import se.leap.bitmaskclient.R;
import se.leap.bitmaskclient.base.FragmentManagerEnhanced;
+import se.leap.bitmaskclient.base.models.Introducer;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.base.models.ProviderObservable;
import se.leap.bitmaskclient.base.utils.ViewHelper;
@@ -50,6 +58,7 @@ import se.leap.bitmaskclient.databinding.ActivitySetupBinding;
import se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog;
import se.leap.bitmaskclient.providersetup.ProviderSetupObservable;
import se.leap.bitmaskclient.providersetup.SetupViewPagerAdapter;
+import se.leap.bitmaskclient.providersetup.fragments.ProviderSelectionFragment;
import se.leap.bitmaskclient.tor.TorServiceCommand;
import se.leap.bitmaskclient.tor.TorStatusObservable;
@@ -73,7 +82,7 @@ public class SetupActivity extends AppCompatActivity implements SetupActivityCal
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
- provider = savedInstanceState.getParcelable(EXTRA_PROVIDER);
+ provider = BundleCompat.getParcelable(savedInstanceState, EXTRA_PROVIDER, Provider.class);
currentPosition = savedInstanceState.getInt(EXTRA_CURRENT_POSITION);
switchProvider = savedInstanceState.getBoolean(EXTRA_SWITCH_PROVIDER);
}
@@ -91,6 +100,13 @@ public class SetupActivity extends AppCompatActivity implements SetupActivityCal
addIndicatorView(indicatorViews);
}
+ if (getIntent() != null) {
+ if (ProviderObservable.getInstance().getCurrentProvider().isConfigured()){
+ switchProvider = true;
+ }
+ manageIntent(getIntent());
+ }
+
// indicator views for config setup
boolean basicProviderSetup = !ProviderObservable.getInstance().getCurrentProvider().isConfigured() || switchProvider;
if (basicProviderSetup) {
@@ -139,11 +155,14 @@ public class SetupActivity extends AppCompatActivity implements SetupActivityCal
});
binding.viewPager.setAdapter(adapter);
binding.viewPager.setUserInputEnabled(false);
- binding.viewPager.setCurrentItem(currentPosition, false);
+
binding.setupNextButton.setOnClickListener(v -> {
int currentPos = binding.viewPager.getCurrentItem();
int newPos = currentPos + 1;
+ if (newPos == CIRCUMVENTION_SETUP_FRAGMENT && provider.hasIntroducer()) {
+ newPos = newPos + 1; // skip configuration of `CIRCUMVENTION_SETUP_FRAGMENT` when invite code provider is selected
+ }
if (newPos >= binding.viewPager.getAdapter().getItemCount()) {
return;
}
@@ -153,6 +172,59 @@ public class SetupActivity extends AppCompatActivity implements SetupActivityCal
cancel();
});
setupActionBar();
+
+ if (ProviderSetupObservable.isSetupRunning()) {
+ provider = BundleCompat.getParcelable(ProviderSetupObservable.getResultData(), PROVIDER_KEY, Provider.class);
+ if (provider != null) {
+ currentPosition = adapter.getFragmentPostion(CONFIGURE_PROVIDER_FRAGMENT);
+ }
+ }
+ binding.viewPager.setCurrentItem(currentPosition, false);
+ }
+
+ /**
+ * Manages the incoming intent and processes the provider selection if the intent action is ACTION_VIEW
+ * and the data scheme is "obfsvpnintro". This method create an introducer from the URI data and sets the
+ * current provider to the introducer.
+ * <p>
+ * If the current fragment is a ProviderSelectionFragment, it will notify the fragment that the provider
+ * selection has changed.
+ * </p>
+ *
+ *
+ * @param intent The incoming intent to be managed.
+ * @see #onProviderSelected(Provider)
+ * @see ProviderSelectionFragment#providerSelectionChanged()
+ */
+ private void manageIntent(Intent intent) {
+ if (Intent.ACTION_VIEW.equals(intent.getAction()) && intent.getData() != null) {
+ String scheme = intent.getData().getScheme();
+
+ if (Objects.equals(scheme, "obfsvpnintro")) {
+ try {
+ onProviderSelected(new Provider(Introducer.fromUrl(intent.getData().toString())));
+ binding.viewPager.setCurrentItem(adapter.getFragmentPostion(PROVIDER_SELECTION_FRAGMENT));
+ binding.viewPager.post(() -> {
+ /**
+ * @see FragmentStateAdapter#saveState()
+ */
+ String fragmentTag = "f" + binding.viewPager.getCurrentItem();
+ Fragment fragment = getSupportFragmentManager().findFragmentByTag(fragmentTag);
+ if (fragment instanceof ProviderSelectionFragment){
+ ((ProviderSelectionFragment) fragment).providerSelectionChanged();
+ }
+ });
+ } catch (Exception e) {
+ Log.d("invite", e.getMessage());
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ manageIntent(intent);
}
@Override
@@ -324,6 +396,7 @@ public class SetupActivity extends AppCompatActivity implements SetupActivityCal
@Override
public void retrySetUpProvider(@NonNull Provider provider) {
+ ProviderSetupObservable.reset();
onProviderSelected(provider);
binding.viewPager.setCurrentItem(adapter.getFragmentPostion(CONFIGURE_PROVIDER_FRAGMENT));
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/CircumventionSetupFragment.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/CircumventionSetupFragment.java
index 58fccc65..d7d8516e 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/CircumventionSetupFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/CircumventionSetupFragment.java
@@ -1,5 +1,6 @@
package se.leap.bitmaskclient.providersetup.fragments;
+import static se.leap.bitmaskclient.base.fragments.CensorshipCircumventionFragment.TUNNELING_AUTOMATICALLY;
import static se.leap.bitmaskclient.base.utils.BuildConfigHelper.isDefaultBitmask;
import android.graphics.Typeface;
@@ -35,14 +36,17 @@ public class CircumventionSetupFragment extends BaseSetupFragment implements Can
if (binding.rbCircumvention.getId() == checkedId) {
PreferenceHelper.useBridges(true);
PreferenceHelper.useSnowflake(true);
+ PreferenceHelper.setUseTunnel(TUNNELING_AUTOMATICALLY);
binding.tvCircumventionDetailDescription.setVisibility(View.VISIBLE);
binding.rbCircumvention.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
binding.rbPlainVpn.setTypeface(Typeface.DEFAULT, Typeface.NORMAL);
return;
}
-
+ // otherwise don't use obfuscation
PreferenceHelper.useBridges(false);
- PreferenceHelper.useSnowflake(false);
+ PreferenceHelper.resetSnowflakeSettings();
+ PreferenceHelper.setUsePortHopping(false);
+ PreferenceHelper.setUseTunnel(TUNNELING_AUTOMATICALLY);
binding.tvCircumventionDetailDescription.setVisibility(View.GONE);
binding.rbPlainVpn.setTypeface(Typeface.DEFAULT, Typeface.BOLD);
binding.rbCircumvention.setTypeface(Typeface.DEFAULT, Typeface.NORMAL);
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ConfigureProviderFragment.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ConfigureProviderFragment.java
index cdb255fc..b9051b1e 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ConfigureProviderFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ConfigureProviderFragment.java
@@ -7,11 +7,16 @@ import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
import static se.leap.bitmaskclient.R.string.app_name;
import static se.leap.bitmaskclient.R.string.description_configure_provider;
import static se.leap.bitmaskclient.R.string.description_configure_provider_circumvention;
+import static se.leap.bitmaskclient.base.fragments.CensorshipCircumventionFragment.TUNNELING_AUTOMATICALLY;
import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_CODE;
import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_KEY;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
import static se.leap.bitmaskclient.base.utils.BuildConfigHelper.isDefaultBitmask;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseSnowflake;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.hasSnowflakePrefs;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.setUsePortHopping;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.setUseTunnel;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useSnowflake;
import static se.leap.bitmaskclient.base.utils.ViewHelper.animateContainerVisibility;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE;
@@ -40,6 +45,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.res.ResourcesCompat;
+import androidx.core.os.BundleCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -47,8 +53,11 @@ import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
+import de.blinkt.openvpn.core.VpnStatus;
import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.models.Constants;
import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
import se.leap.bitmaskclient.databinding.FConfigureProviderBinding;
import se.leap.bitmaskclient.eip.EipSetupListener;
import se.leap.bitmaskclient.eip.EipSetupObserver;
@@ -123,7 +132,7 @@ public class ConfigureProviderFragment extends BaseSetupFragment implements Prop
public void onFragmentSelected() {
super.onFragmentSelected();
ignoreProviderAPIUpdates = false;
- binding.detailContainer.setVisibility(getUseSnowflake() ? VISIBLE : GONE);
+ binding.detailContainer.setVisibility(!VpnStatus.isVPNActive() && hasSnowflakePrefs() && getUseSnowflake() ? VISIBLE : GONE);
binding.tvCircumventionDescription.setText(getUseSnowflake() ? getString(description_configure_provider_circumvention, getString(app_name)) : getString(description_configure_provider, getString(app_name)));
if (!isDefaultBitmask()) {
Drawable drawable = ResourcesCompat.getDrawable(getResources(), R.drawable.setup_progress_spinner, null);
@@ -132,8 +141,22 @@ public class ConfigureProviderFragment extends BaseSetupFragment implements Prop
binding.progressSpinner.update(ProviderSetupObservable.getProgress());
setupActivityCallback.setNavigationButtonHidden(true);
setupActivityCallback.setCancelButtonHidden(false);
- ProviderSetupObservable.startSetup();
- ProviderAPICommand.execute(getContext(), SET_UP_PROVIDER, setupActivityCallback.getSelectedProvider());
+ if (ProviderSetupObservable.isSetupRunning()) {
+ handleResult(ProviderSetupObservable.getResultCode(), ProviderSetupObservable.getResultData(), true);
+ } else {
+ Provider provider = setupActivityCallback.getSelectedProvider();
+ if (provider != null && provider.hasIntroducer()) {
+ // enable automatic selection of bridges
+ useSnowflake(false);
+ setUseTunnel(TUNNELING_AUTOMATICALLY);
+ setUsePortHopping(false);
+ PreferenceHelper.useBridges(true);
+ }
+ ProviderSetupObservable.startSetup();
+ Bundle parameters = new Bundle();
+ parameters.putString(Constants.COUNTRYCODE, PreferenceHelper.getBaseCountry());
+ ProviderAPICommand.execute(getContext(), SET_UP_PROVIDER, parameters, setupActivityCallback.getSelectedProvider());
+ }
}
protected void showConnectionDetails() {
@@ -203,31 +226,35 @@ public class ConfigureProviderFragment extends BaseSetupFragment implements Prop
if (resultData == null) {
resultData = Bundle.EMPTY;
}
- Provider provider = resultData.getParcelable(PROVIDER_KEY);
+ handleResult(resultCode, resultData, false);
+ }
+
+ private void handleResult(int resultCode, Bundle resultData, boolean resumeSetup) {
+ Provider provider = BundleCompat.getParcelable(resultData, PROVIDER_KEY, Provider.class);
+
if (ignoreProviderAPIUpdates ||
provider == null ||
(setupActivityCallback.getSelectedProvider() != null &&
- !setupActivityCallback.getSelectedProvider().getMainUrlString().equals(provider.getMainUrlString()))) {
+ !setupActivityCallback.getSelectedProvider().getMainUrl().equals(provider.getMainUrl()))) {
return;
}
switch (resultCode) {
case PROVIDER_OK:
- if (provider.allowsAnonymous()) {
- ProviderAPICommand.execute(this.getContext(), DOWNLOAD_VPN_CERTIFICATE, provider);
+ setupActivityCallback.onProviderSelected(provider);
+ if (provider.getApiVersion() < 5) {
+ if (provider.allowsAnonymous()) {
+ ProviderAPICommand.execute(this.getContext(), DOWNLOAD_VPN_CERTIFICATE, provider);
+ } else {
+ // TODO: implement error message that this client only supports anonymous usage
+ }
} else {
- // TODO: implement error message that this client only supports anonymous usage
+ sendSuccess(resumeSetup);
}
break;
case CORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
setupActivityCallback.onProviderSelected(provider);
- handler.postDelayed(() -> {
- if (!ProviderSetupObservable.isCanceled()) {
- if (setupActivityCallback != null) {
- setupActivityCallback.onConfigurationSuccess();
- }
- }
- }, 750);
+ sendSuccess(resumeSetup);
break;
case PROVIDER_NOK:
case INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
@@ -241,4 +268,15 @@ public class ConfigureProviderFragment extends BaseSetupFragment implements Prop
}
}
+ private void sendSuccess(boolean resumeSetup) {
+ handler.postDelayed(() -> {
+ if (!ProviderSetupObservable.isCanceled()) {
+ try {
+ setupActivityCallback.onConfigurationSuccess();
+ } catch (NullPointerException npe) {
+ // callback disappeared in the meanwhile
+ }
+ }
+ }, resumeSetup ? 0 : 750);
+ }
} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/EmptyPermissionSetupFragment.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/EmptyPermissionSetupFragment.java
index 849ac681..c6671a90 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/EmptyPermissionSetupFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/EmptyPermissionSetupFragment.java
@@ -13,6 +13,7 @@ import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.os.BundleCompat;
import se.leap.bitmaskclient.R;
import se.leap.bitmaskclient.databinding.FEmptyPermissionSetupBinding;
@@ -76,7 +77,7 @@ public class EmptyPermissionSetupFragment extends BaseSetupFragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- this.vpnPermissionIntent = getArguments().getParcelable(EXTRA_VPN_INTENT);
+ this.vpnPermissionIntent = BundleCompat.getParcelable(getArguments(), EXTRA_VPN_INTENT, Intent.class);
this.notificationPermissionAction = getArguments().getString(EXTRA_NOTIFICATION_PERMISSON_ACTION);
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/NotificationSetupFragment.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/NotificationSetupFragment.java
deleted file mode 100644
index a9589336..00000000
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/NotificationSetupFragment.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package se.leap.bitmaskclient.providersetup.fragments;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import se.leap.bitmaskclient.databinding.FNotificationSetupBinding;
-
-public class NotificationSetupFragment extends BaseSetupFragment {
-
- public static NotificationSetupFragment newInstance(int position) {
- NotificationSetupFragment fragment = new NotificationSetupFragment();
- fragment.setArguments(initBundle(position));
- return fragment;
- }
-
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- FNotificationSetupBinding binding = FNotificationSetupBinding.inflate(inflater, container, false);
- return binding.getRoot();
- }
-
- @Override
- public void onFragmentSelected() {
- super.onFragmentSelected();
- setupActivityCallback.setNavigationButtonHidden(false);
- setupActivityCallback.setCancelButtonHidden(true);
- }
-
-} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/PermissionExplanationFragment.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/PermissionExplanationFragment.java
new file mode 100644
index 00000000..4ac69ee8
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/PermissionExplanationFragment.java
@@ -0,0 +1,72 @@
+package se.leap.bitmaskclient.providersetup.fragments;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static se.leap.bitmaskclient.base.utils.BuildConfigHelper.isDefaultBitmask;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.databinding.FPermissionExplanationBinding;
+
+
+public class PermissionExplanationFragment extends BaseSetupFragment {
+
+ private static String EXTRA_SHOW_NOTIFICATION_PERMISSION = "EXTRA_SHOW_NOTIFICATION_PERMISSION";
+ private static String EXTRA_SHOW_VPN_PERMISSION = "EXTRA_SHOW_VPN_PERMISSION";
+ FPermissionExplanationBinding binding;
+ public static PermissionExplanationFragment newInstance(int position, boolean showNotificationPermission, boolean showVpnPermission) {
+ PermissionExplanationFragment fragment = new PermissionExplanationFragment();
+ Bundle bundle = initBundle(position);
+ bundle.putBoolean(EXTRA_SHOW_NOTIFICATION_PERMISSION, showNotificationPermission);
+ bundle.putBoolean(EXTRA_SHOW_VPN_PERMISSION, showVpnPermission);
+ fragment.setArguments(bundle);
+ return fragment;
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ binding = FPermissionExplanationBinding.inflate(inflater, container, false);
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ if (getArguments() != null) {
+ boolean showNotificationPermission = getArguments().getBoolean(EXTRA_SHOW_NOTIFICATION_PERMISSION);
+ boolean showVpnPermission = getArguments().getBoolean(EXTRA_SHOW_VPN_PERMISSION);
+ if (showVpnPermission && showNotificationPermission) {
+ binding.tvTitle.setText(R.string.title_upcoming_request);
+ binding.titleUpcomingRequestSummary.setVisibility(VISIBLE);
+ } else if (showVpnPermission) {
+ binding.tvTitle.setText(R.string.title_upcoming_connection_request);
+ binding.titleUpcomingRequestSummary.setVisibility(GONE);
+ } else if (showNotificationPermission) {
+ binding.tvTitle.setText(R.string.title_upcoming_notifications_request);
+ binding.titleUpcomingRequestSummary.setVisibility(GONE);
+ }
+
+ binding.titleUpcomingNotificationRequestSummary.setVisibility(showNotificationPermission ? VISIBLE: GONE);
+ binding.titleUpcomingConnectionRequestSummary.setText(isDefaultBitmask() ?
+ getString(R.string.title_upcoming_connection_request_summary) :
+ getString(R.string.title_upcoming_connection_request_summary_custom, getString(R.string.app_name)));
+ binding.titleUpcomingConnectionRequestSummary.setVisibility(showVpnPermission ? VISIBLE : GONE);
+ }
+ }
+
+ @Override
+ public void onFragmentSelected() {
+ super.onFragmentSelected();
+ setupActivityCallback.setNavigationButtonHidden(false);
+ setupActivityCallback.setCancelButtonHidden(true);
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java
index f15aaa43..f3da117b 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java
@@ -1,6 +1,7 @@
package se.leap.bitmaskclient.providersetup.fragments;
import static se.leap.bitmaskclient.providersetup.fragments.viewmodel.ProviderSelectionViewModel.ADD_PROVIDER;
+import static se.leap.bitmaskclient.providersetup.fragments.viewmodel.ProviderSelectionViewModel.INVITE_CODE_PROVIDER;
import android.graphics.Typeface;
import android.os.Bundle;
@@ -15,22 +16,27 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
+import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.models.Introducer;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.base.utils.ViewHelper;
import se.leap.bitmaskclient.databinding.FProviderSelectionBinding;
import se.leap.bitmaskclient.providersetup.activities.CancelCallback;
+import se.leap.bitmaskclient.providersetup.fragments.helpers.AbstractQrScannerHelper;
+import se.leap.bitmaskclient.providersetup.helpers.QrScannerHelper;
import se.leap.bitmaskclient.providersetup.fragments.viewmodel.ProviderSelectionViewModel;
import se.leap.bitmaskclient.providersetup.fragments.viewmodel.ProviderSelectionViewModelFactory;
-public class ProviderSelectionFragment extends BaseSetupFragment implements CancelCallback {
+public class ProviderSelectionFragment extends BaseSetupFragment implements CancelCallback, AbstractQrScannerHelper.ScanResultCallback {
private ProviderSelectionViewModel viewModel;
private ArrayList<RadioButton> radioButtons;
private FProviderSelectionBinding binding;
+ private QrScannerHelper qrScannerHelper;
public static ProviderSelectionFragment newInstance(int position) {
ProviderSelectionFragment fragment = new ProviderSelectionFragment();
@@ -46,6 +52,8 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc
new ProviderSelectionViewModelFactory(
getContext().getApplicationContext().getAssets())).
get(ProviderSelectionViewModel.class);
+
+ qrScannerHelper = new QrScannerHelper(this, this);
}
@Override
@@ -54,6 +62,7 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc
binding = FProviderSelectionBinding.inflate(inflater, container, false);
radioButtons = new ArrayList<>();
+ // add configured providers
for (int i = 0; i < viewModel.size(); i++) {
RadioButton radioButton = new RadioButton(binding.getRoot().getContext());
radioButton.setText(viewModel.getProviderName(i));
@@ -61,13 +70,24 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc
binding.providerRadioGroup.addView(radioButton);
radioButtons.add(radioButton);
}
- RadioButton radioButton = new RadioButton(binding.getRoot().getContext());
- radioButton.setText(getText(R.string.add_provider));
- radioButton.setId(ADD_PROVIDER);
- binding.providerRadioGroup.addView(radioButton);
- radioButtons.add(radioButton);
+
+ // add new provider entry
+ RadioButton addProviderRadioButton = new RadioButton(binding.getRoot().getContext());
+ addProviderRadioButton.setText(getText(R.string.add_provider));
+ addProviderRadioButton.setId(ADD_PROVIDER);
+ binding.providerRadioGroup.addView(addProviderRadioButton);
+ radioButtons.add(addProviderRadioButton);
+
+ // invite code entry
+ RadioButton inviteCodeRadioButton = new RadioButton(binding.getRoot().getContext());
+ inviteCodeRadioButton.setText(R.string.enter_invite_code);
+ inviteCodeRadioButton.setId(INVITE_CODE_PROVIDER);
+ binding.providerRadioGroup.addView(inviteCodeRadioButton);
+ radioButtons.add(inviteCodeRadioButton);
binding.editCustomProvider.setVisibility(viewModel.getEditProviderVisibility());
+ binding.syntaxCheck.setVisibility(viewModel.getEditProviderVisibility());
+ binding.qrScanner.setVisibility(viewModel.getQrScannerVisibility());
return binding.getRoot();
}
@@ -75,8 +95,14 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupActivityCallback.registerCancelCallback(this);
+ initQrScanner();
+ }
+
+ private void initQrScanner() {
+ binding.btnQrScanner.setOnClickListener(v -> qrScannerHelper.startScan());
}
+
@Override
public void onFragmentSelected() {
super.onFragmentSelected();
@@ -89,11 +115,23 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc
}
binding.providerDescription.setText(viewModel.getProviderDescription(getContext()));
binding.editCustomProvider.setVisibility(viewModel.getEditProviderVisibility());
+ binding.syntaxCheck.setVisibility(viewModel.getEditProviderVisibility());
+ binding.qrScanner.setVisibility(viewModel.getQrScannerVisibility());
+ if (viewModel.getCustomUrl() == null || viewModel.getCustomUrl().isEmpty()) {
+ binding.syntaxCheckResult.setText("");
+ binding.syntaxCheckResult.setTextColor(getResources().getColor(R.color.color_font_btn));
+ binding.editCustomProvider.setHint(viewModel.getHint(getContext()));
+ } else {
+ binding.editCustomProvider.setText("");
+ }
+ binding.editCustomProvider.setRawInputType(viewModel.getEditInputType());
+ binding.editCustomProvider.setMaxLines(viewModel.getEditInputLines());
+ binding.editCustomProvider.setMinLines(viewModel.getEditInputLines());
setupActivityCallback.onSetupStepValidationChanged(viewModel.isValidConfig());
- if (checkedId != ADD_PROVIDER) {
+ if (checkedId != ADD_PROVIDER && checkedId != INVITE_CODE_PROVIDER) {
setupActivityCallback.onProviderSelected(viewModel.getProvider(checkedId));
} else if (viewModel.isValidConfig()) {
- setupActivityCallback.onProviderSelected(new Provider(binding.editCustomProvider.getText().toString()));
+ providerSelected(binding.editCustomProvider.getText().toString(),checkedId);
}
});
@@ -107,7 +145,12 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc
if (viewModel.isCustomProviderSelected()) {
setupActivityCallback.onSetupStepValidationChanged(viewModel.isValidConfig());
if (viewModel.isValidConfig()) {
- setupActivityCallback.onProviderSelected(new Provider(viewModel.getCustomUrl()));
+ providerSelected(viewModel.getCustomUrl(),viewModel.getSelected());
+ binding.syntaxCheckResult.setText(getString(R.string.validation_status_success));
+ binding.syntaxCheckResult.setTextColor(getResources().getColor(R.color.green200));
+ } else {
+ binding.syntaxCheckResult.setText(getString(R.string.validation_status_failure));
+ binding.syntaxCheckResult.setTextColor(getResources().getColor(R.color.red200));
}
}
}
@@ -127,7 +170,33 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc
binding.getRoot().smoothScrollTo(binding.editCustomProvider.getLeft(), binding.getRoot().getBottom());
}
});
- binding.providerRadioGroup.check(viewModel.getSelected());
+ providerSelectionChanged();
+ }
+
+ public void providerSelectionChanged(){
+ Provider provider = setupActivityCallback.getSelectedProvider();
+ if (provider != null && provider.hasIntroducer()) {
+ try {
+ binding.providerRadioGroup.check(INVITE_CODE_PROVIDER);
+ binding.editCustomProvider.setText(provider.getIntroducer().toUrl());
+ } catch (UnsupportedEncodingException e) {
+ e.printStackTrace();
+ }
+ } else {
+ binding.providerRadioGroup.check(viewModel.getSelected());
+ }
+ }
+
+ private void providerSelected(String string, int checkedId) {
+ if (checkedId == INVITE_CODE_PROVIDER) {
+ try {
+ setupActivityCallback.onProviderSelected(new Provider(Introducer.fromUrl(string)));
+ } catch (Exception e) {
+ // This cannot happen
+ }
+ } else {
+ setupActivityCallback.onProviderSelected(new Provider(string));
+ }
}
@Override
@@ -153,4 +222,13 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc
public void onCanceled() {
binding.providerRadioGroup.check(0);
}
+
+ @Override
+ public void onScanResult(Introducer introducer) {
+ try {
+ binding.editCustomProvider.setText(introducer.toUrl());
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/SetupFragmentFactory.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/SetupFragmentFactory.java
index eaf3fbfa..13d1e5ff 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/SetupFragmentFactory.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/SetupFragmentFactory.java
@@ -11,21 +11,23 @@ import java.util.ArrayList;
public class SetupFragmentFactory {
public static final int PROVIDER_SELECTION_FRAGMENT = 0;
public static final int CIRCUMVENTION_SETUP_FRAGMENT = 1;
- public static final int CONFIGURE_PROVIDER_FRAGMENT = 2;
- public static final int VPN_PERMISSON_EDUCATIONAL_FRAGMENT = 3;
- public static final int VPN_PERMISSON_FRAGMENT = 4;
- public static final int NOTIFICATION_PERMISSON_EDUCATIONAL_FRAGMENT = 5;
- public static final int NOTIFICATION_PERMISSON_FRAGMENT = 6;
+ public static final int VPN_PERMISSON_FRAGMENT = 2;
+ public static final int NOTIFICATION_PERMISSON_EDUCATIONAL_FRAGMENT = 3;
+ public static final int NOTIFICATION_PERMISSON_FRAGMENT = 4;
+ public static final int CONFIGURE_PROVIDER_FRAGMENT = 5;
- public static final int SUCCESS_FRAGMENT = 7;
+ public static final int SUCCESS_FRAGMENT = 6;
private final Intent vpnPermissionRequest;
private final ArrayList<Integer> fragmentTypes;
- public SetupFragmentFactory(@NonNull ArrayList<Integer> fragmentTypes, Intent vpnPermissionRequest) {
+ private final boolean showNotificationPermission;
+
+ public SetupFragmentFactory(@NonNull ArrayList<Integer> fragmentTypes, Intent vpnPermissionRequest, boolean showNotificationPermission) {
this.fragmentTypes = fragmentTypes;
this.vpnPermissionRequest = vpnPermissionRequest;
+ this.showNotificationPermission = showNotificationPermission;
}
public Fragment createFragment(int position) {
@@ -41,11 +43,9 @@ public class SetupFragmentFactory {
case CONFIGURE_PROVIDER_FRAGMENT:
return ConfigureProviderFragment.newInstance(position);
case NOTIFICATION_PERMISSON_EDUCATIONAL_FRAGMENT:
- return NotificationSetupFragment.newInstance(position);
+ return PermissionExplanationFragment.newInstance(position, showNotificationPermission, vpnPermissionRequest!=null);
case NOTIFICATION_PERMISSON_FRAGMENT:
return EmptyPermissionSetupFragment.newInstance(position, Manifest.permission.POST_NOTIFICATIONS);
- case VPN_PERMISSON_EDUCATIONAL_FRAGMENT:
- return VpnPermissionSetupFragment.newInstance(position);
case VPN_PERMISSON_FRAGMENT:
return EmptyPermissionSetupFragment.newInstance(position, vpnPermissionRequest);
case SUCCESS_FRAGMENT:
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/VpnPermissionSetupFragment.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/VpnPermissionSetupFragment.java
deleted file mode 100644
index 188ba9ac..00000000
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/VpnPermissionSetupFragment.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package se.leap.bitmaskclient.providersetup.fragments;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import se.leap.bitmaskclient.databinding.FVpnPermissionSetupBinding;
-
-public class VpnPermissionSetupFragment extends BaseSetupFragment {
-
- public static VpnPermissionSetupFragment newInstance(int position) {
- VpnPermissionSetupFragment fragment = new VpnPermissionSetupFragment();
- fragment.setArguments(initBundle(position));
- return fragment;
- }
-
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- FVpnPermissionSetupBinding binding = FVpnPermissionSetupBinding.inflate(inflater, container, false);
- return binding.getRoot();
- }
-
- @Override
- public void onFragmentSelected() {
- super.onFragmentSelected();
- setupActivityCallback.setNavigationButtonHidden(false);
- setupActivityCallback.setCancelButtonHidden(true);
- }
-
-} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/helpers/AbstractQrScannerHelper.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/helpers/AbstractQrScannerHelper.java
new file mode 100644
index 00000000..132d8cc9
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/helpers/AbstractQrScannerHelper.java
@@ -0,0 +1,16 @@
+package se.leap.bitmaskclient.providersetup.fragments.helpers;
+
+import androidx.fragment.app.Fragment;
+
+import se.leap.bitmaskclient.base.models.Introducer;
+
+public abstract class AbstractQrScannerHelper {
+ public interface ScanResultCallback {
+ void onScanResult(Introducer introducer);
+ }
+
+ public AbstractQrScannerHelper(Fragment fragment, ScanResultCallback callback) {
+ }
+
+ public abstract void startScan();
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java
index 29dab98a..954c9301 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java
@@ -1,22 +1,26 @@
package se.leap.bitmaskclient.providersetup.fragments.viewmodel;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.isDomainName;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.isNetworkUrl;
+
import android.content.Context;
import android.content.res.AssetManager;
-import android.util.Patterns;
+import android.text.InputType;
import android.view.View;
-import android.webkit.URLUtil;
import androidx.lifecycle.ViewModel;
import java.util.List;
import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.models.Introducer;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.providersetup.ProviderManager;
public class ProviderSelectionViewModel extends ViewModel {
private final ProviderManager providerManager;
public static int ADD_PROVIDER = 100100100;
+ public static int INVITE_CODE_PROVIDER = 200100100;
private int selected = 0;
private String customUrl;
@@ -48,19 +52,31 @@ public class ProviderSelectionViewModel extends ViewModel {
public boolean isValidConfig() {
if (selected == ADD_PROVIDER) {
- return customUrl != null && (Patterns.DOMAIN_NAME.matcher(customUrl).matches() || (URLUtil.isNetworkUrl(customUrl) && Patterns.WEB_URL.matcher(customUrl).matches()));
+ return isNetworkUrl(customUrl) || isDomainName(customUrl);
+ }
+ if (selected == INVITE_CODE_PROVIDER) {
+ try {
+ Introducer introducer = Introducer.fromUrl(customUrl);
+ return introducer.validate();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
}
return true;
}
public boolean isCustomProviderSelected() {
- return selected == ADD_PROVIDER;
+ return selected == ADD_PROVIDER || selected == INVITE_CODE_PROVIDER;
}
public CharSequence getProviderDescription(Context context) {
if (selected == ADD_PROVIDER) {
return context.getText(R.string.add_provider_description);
}
+ if (selected == INVITE_CODE_PROVIDER) {
+ return context.getText(R.string.invite_code_provider_description);
+ }
Provider provider = getProvider(selected);
if ("riseup.net".equals(provider.getDomain())) {
return context.getText(R.string.provider_description_riseup);
@@ -71,19 +87,42 @@ public class ProviderSelectionViewModel extends ViewModel {
return provider.getDescription();
}
+ public int getQrScannerVisibility() {
+ if (selected == INVITE_CODE_PROVIDER) {
+ return View.VISIBLE;
+ }
+ return View.GONE;
+ }
+
public int getEditProviderVisibility() {
if (selected == ADD_PROVIDER) {
return View.VISIBLE;
+ } else if (selected == INVITE_CODE_PROVIDER) {
+ return View.VISIBLE;
}
return View.GONE;
}
+ public int getEditInputType() {
+ if (selected == INVITE_CODE_PROVIDER) {
+ return InputType.TYPE_TEXT_FLAG_MULTI_LINE;
+ }
+ return InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
+ }
+
+ public int getEditInputLines() {
+ if (selected == INVITE_CODE_PROVIDER) {
+ return 3;
+ }
+ return 1;
+ }
+
public void setCustomUrl(String url) {
customUrl = url;
}
public String getCustomUrl() {
- if (customUrl != null && Patterns.DOMAIN_NAME.matcher(customUrl).matches()) {
+ if (isDomainName(customUrl)) {
return "https://" + customUrl;
}
return customUrl;
@@ -92,7 +131,7 @@ public class ProviderSelectionViewModel extends ViewModel {
public String getProviderName(int pos) {
String domain = getProvider(pos).getDomain();
- if ("riseup.net".equals(domain)) {
+ if ("riseup.net".equals(domain) || "black.riseup.net".equals(domain)) {
return "Riseup";
}
if ("calyx.net".equals(domain)) {
@@ -100,4 +139,14 @@ public class ProviderSelectionViewModel extends ViewModel {
}
return domain;
}
+
+ public CharSequence getHint(Context context) {
+ if (selected == ADD_PROVIDER) {
+ return context.getText(R.string.add_provider_prompt);
+ }
+ if (selected == INVITE_CODE_PROVIDER) {
+ return context.getText(R.string.invite_code_provider_prompt);
+ }
+ return "";
+ }
} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java b/app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java
index f13eb70e..b7909865 100644
--- a/app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java
+++ b/app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java
@@ -1,6 +1,6 @@
package se.leap.bitmaskclient.tor;
/**
- * Copyright (c) 2021 LEAP Encryption Access Project and contributors
+ * Copyright (c) 2024 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
@@ -66,6 +66,7 @@ public class ClientTransportPlugin implements ClientTransportPluginInterface, Pr
this.contextRef = new WeakReference<>(context);
handlerThread = new HandlerThread("clientTransportPlugin", Thread.MIN_PRIORITY);
loadCdnFronts(context);
+ IPtProxy.setStateLocation(context.getApplicationContext().getCacheDir() + "/pt_state");
}
@Override
@@ -96,18 +97,39 @@ public class ClientTransportPlugin implements ClientTransportPluginInterface, Pr
private void startConnectionAttempt(boolean useAmpCache, @NonNull String logfilePath) {
//this is using the current, default Tor snowflake infrastructure
String target = getCdnFront("snowflake-target");
- String front = getCdnFront("snowflake-front");
+ String fronts = getCdnFront("snowflake-fronts");
String stunServer = getCdnFront("snowflake-stun");
String ampCache = null;
if (useAmpCache) {
target = "https://snowflake-broker.torproject.net/";
ampCache = "https://cdn.ampproject.org/";
- front = "www.google.com";
+ fronts = "www.google.com";
}
- snowflakePort = IPtProxy.startSnowflake(stunServer, target, front, ampCache, logfilePath, false, false, true, 5);
+
+ snowflakePort = startSnowflake(stunServer, target, fronts, ampCache, null, null, logfilePath, false, false, false, 5);
Log.d(TAG, "startSnowflake running on port: " + snowflakePort);
}
+/**
+ StartSnowflake - Start IPtProxy's Snowflake client.
+ @param ice Comma-separated list of ICE servers.
+ @param url URL of signaling broker.
+ @param fronts Comma-separated list of front domains.
+ @param ampCache OPTIONAL. URL of AMP cache to use as a proxy for signaling.
+ Only needed when you want to do the rendezvous over AMP instead of a domain fronted server.
+ @param sqsQueueURL OPTIONAL. URL of SQS Queue to use as a proxy for signaling.
+ @param sqsCredsStr OPTIONAL. Credentials to access SQS Queue
+ @param logFile Name of log file. OPTIONAL. Defaults to no log.
+ @param logToStateDir Resolve the log file relative to Tor's PT state dir.
+ @param keepLocalAddresses Keep local LAN address ICE candidates.
+ @param unsafeLogging Prevent logs from being scrubbed.
+ @param maxPeers Capacity for number of multiplexed WebRTC peers. DEFAULTs to 1 if less than that.
+ @return Port number where Snowflake will listen on, if no error happens during start up.
+ */
+ private long startSnowflake(String ice, String url, String fronts, String ampCache, String sqsQueueURL, String sqsCredsStr, String logFile, boolean logToStateDir, boolean keepLocalAddresses, boolean unsafeLogging, long maxPeers) {
+ return IPtProxy.startSnowflake(ice, url, fronts, ampCache, sqsQueueURL, sqsCredsStr, logFile, logToStateDir, keepLocalAddresses, unsafeLogging, maxPeers);
+ }
+
private void retryConnectionAttempt(boolean useAmpCache) {
Log.d(TAG, ">> retryConnectionAttempt - " + (useAmpCache ? "amp cache" : "http domain fronting"));
stopConnectionAttempt();
diff --git a/app/src/main/java/se/leap/bitmaskclient/tor/TorServiceCommand.java b/app/src/main/java/se/leap/bitmaskclient/tor/TorServiceCommand.java
index abc029ff..4c6ddaba 100644
--- a/app/src/main/java/se/leap/bitmaskclient/tor/TorServiceCommand.java
+++ b/app/src/main/java/se/leap/bitmaskclient/tor/TorServiceCommand.java
@@ -134,6 +134,21 @@ public class TorServiceCommand {
return -1;
}
+ @WorkerThread
+ public static int getSocksProxyPort(Context context) {
+ try {
+ TorServiceConnection torServiceConnection = initTorServiceConnection(context);
+ if (torServiceConnection != null) {
+ int tunnelPort = torServiceConnection.getService().getSocksPort();
+ torServiceConnection.close();
+ return tunnelPort;
+ }
+ } catch (InterruptedException | IllegalStateException e) {
+ e.printStackTrace();
+ }
+ return -1;
+ }
+
private static boolean isNotCancelled() {
return !TorStatusObservable.isCancelled();
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java b/app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java
index b1ad6084..5a49fda2 100644
--- a/app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java
+++ b/app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java
@@ -95,6 +95,7 @@ public class TorStatusObservable {
private String lastTorLog = "";
private String lastSnowflakeLog = "";
private int port = -1;
+ private int socksPort = -1;
private int bootstrapPercent = -1;
private int retrySnowflakeRendezVous = 0;
private final Vector<String> lastLogs = new Vector<>(100);
@@ -321,6 +322,15 @@ public class TorStatusObservable {
return getInstance().port;
}
+ public static void setSocksProxyPort(int port) {
+ getInstance().socksPort = port;
+ instance.notifyObservers();
+ }
+
+ public static int getSocksProxyPort() {
+ return getInstance().socksPort;
+ }
+
@Nullable
public static String getLastTorLog() {