summaryrefslogtreecommitdiff
path: root/app/src/main/java/se/leap
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/se/leap')
-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
69 files changed, 4645 insertions, 2259 deletions
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() {