diff options
Diffstat (limited to 'app/src/main/java/se/leap')
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() { |
