diff options
author | Norbel Ambanumben <nambanumben@riseup.net> | 2024-10-11 11:54:39 +0000 |
---|---|---|
committer | cyberta <cyberta@riseup.net> | 2024-10-11 11:54:39 +0000 |
commit | 55219121b127a000b2d410a841b6441a25adb1f3 (patch) | |
tree | de5e801f2df0cccf393a051976a12a7a69fcc442 /app/src/main/java | |
parent | 236ce1209278fae174713d605300b6ed3f16febb (diff) |
feat: add invite code to UI.
Diffstat (limited to 'app/src/main/java')
5 files changed, 212 insertions, 9 deletions
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..c31912d9 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Introducer.java @@ -0,0 +1,105 @@ +package se.leap.bitmaskclient.base.models; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.net.URI; +import java.net.URISyntaxException; + +public class Introducer implements Parcelable { + private String type; + private String address; + private String certificate; + private String fullyQualifiedDomainName; + private boolean kcpEnabled; + + public Introducer(String type, String address, String certificate, String fullyQualifiedDomainName, boolean kcpEnabled) { + this.type = type; + this.address = address; + this.certificate = certificate; + this.fullyQualifiedDomainName = fullyQualifiedDomainName; + this.kcpEnabled = kcpEnabled; + } + + protected Introducer(Parcel in) { + type = in.readString(); + address = in.readString(); + certificate = in.readString(); + fullyQualifiedDomainName = in.readString(); + kcpEnabled = in.readByte() != 0; + } + + @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)); + } + + @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); + } + return true; + } + + public static Introducer fromUrl(String introducerUrl) throws URISyntaxException { + URI uri = new URI(introducerUrl); + String fqdn = getQueryParam(uri, "fqdn"); + if (fqdn == null || fqdn.isEmpty()) { + throw new IllegalArgumentException("FQDN not found in the introducer URL"); + } + + boolean kcp = "1".equals(getQueryParam(uri, "kcp")); + + String cert = getQueryParam(uri, "cert"); + if (cert == null || cert.isEmpty()) { + throw new IllegalArgumentException("Cert not found in the introducer URL"); + } + + return new Introducer(uri.getScheme(), uri.getAuthority(), cert, fqdn, kcp); + } + + public String toUrl() { + return String.format("%s://%s?fqdn=%s&kcp=%d&cert=%s", type, address, fullyQualifiedDomainName, kcpEnabled ? 1 : 0, certificate); + } + + private static String getQueryParam(URI uri, String param) { + String[] queryParams = uri.getQuery().split("&"); + for (String queryParam : queryParams) { + String[] keyValue = queryParam.split("="); + if (keyValue.length == 2 && keyValue[0].equals(param)) { + return keyValue[1]; + } + } + return null; + } +}
\ 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 725c602a..bf1e6b94 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 @@ -86,6 +86,7 @@ public final class Provider implements Parcelable { 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; @@ -115,6 +116,11 @@ public final class Provider implements Parcelable { public Provider() { } + public Provider(Introducer introducer) { + this(introducer.toUrl(), null); + this.introducer = introducer; + } + public Provider(String mainUrl) { this(mainUrl, null); } @@ -423,6 +429,7 @@ public final class Provider implements Parcelable { parcel.writeLong(lastMotdSeen); parcel.writeStringList(new ArrayList<>(lastMotdSeenHashes)); parcel.writeInt(shouldUpdateVpnCertificate ? 0 : 1); + parcel.writeParcelable(introducer, 0); } @@ -484,6 +491,7 @@ 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()); } catch (MalformedURLException | JSONException e) { e.printStackTrace(); } @@ -739,6 +747,10 @@ public final class Provider implements Parcelable { return getCertificatePinEncoding() + ":" + getCertificatePin(); } + public boolean hasIntroducer() { + return introducer != null; + } + /** * resets everything except the main url, the providerIp and the geoip * service url (currently preseeded) 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 9a0bf7b7..9284edc9 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 @@ -6,7 +6,9 @@ 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.viewmodel.ProviderSelectionViewModel.INVITE_CODE_PROVIDER; import static se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus.OFF; import android.Manifest; @@ -145,6 +147,9 @@ public class SetupActivity extends AppCompatActivity implements SetupActivityCal 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; } 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..24da4691 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,9 +16,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.ViewModelProvider; +import java.net.URISyntaxException; 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; @@ -61,13 +64,22 @@ 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); + + 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); + + + 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()); return binding.getRoot(); } @@ -89,11 +101,22 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc } binding.providerDescription.setText(viewModel.getProviderDescription(getContext())); binding.editCustomProvider.setVisibility(viewModel.getEditProviderVisibility()); + binding.syntaxCheck.setVisibility(viewModel.getEditProviderVisibility()); + 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 +130,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)); } } } @@ -130,6 +158,18 @@ public class ProviderSelectionFragment extends BaseSetupFragment implements Canc 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 public void onDestroyView() { setupActivityCallback.removeCancelCallback(this); 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..58b43fbd 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 @@ -2,6 +2,7 @@ package se.leap.bitmaskclient.providersetup.fragments.viewmodel; import android.content.Context; import android.content.res.AssetManager; +import android.text.InputType; import android.util.Patterns; import android.view.View; import android.webkit.URLUtil; @@ -11,12 +12,14 @@ 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; @@ -50,17 +53,29 @@ public class ProviderSelectionViewModel extends ViewModel { if (selected == ADD_PROVIDER) { return customUrl != null && (Patterns.DOMAIN_NAME.matcher(customUrl).matches() || (URLUtil.isNetworkUrl(customUrl) && Patterns.WEB_URL.matcher(customUrl).matches())); } + 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); @@ -74,9 +89,25 @@ public class ProviderSelectionViewModel extends ViewModel { 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; @@ -100,4 +131,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 |