summaryrefslogtreecommitdiff
path: root/app/src
diff options
context:
space:
mode:
Diffstat (limited to 'app/src')
-rw-r--r--app/src/custom/assets/riseup.net.json52
-rw-r--r--app/src/custom/java/se/leap/bitmaskclient/providersetup/helpers/QrScannerHelper.java16
-rw-r--r--app/src/custom/res/values-ar/strings.xml4
-rw-r--r--app/src/custom/res/values-cs/strings.xml5
-rw-r--r--app/src/custom/res/values-it/strings.xml4
-rw-r--r--app/src/custom/res/values-uk/strings.xml5
-rw-r--r--app/src/main/AndroidManifest.xml32
-rw-r--r--app/src/main/java/de/blinkt/openvpn/VpnProfile.java51
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java79
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java5
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java57
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java2
-rw-r--r--app/src/main/java/io/swagger/client/JSON.java399
-rw-r--r--app/src/main/java/io/swagger/client/model/ModelsBridge.java428
-rw-r--r--app/src/main/java/io/swagger/client/model/ModelsEIPService.java206
-rw-r--r--app/src/main/java/io/swagger/client/model/ModelsGateway.java371
-rw-r--r--app/src/main/java/io/swagger/client/model/ModelsLocation.java301
-rw-r--r--app/src/main/java/io/swagger/client/model/ModelsProvider.java584
-rw-r--r--app/src/main/java/io/swagger/client/model/ModelsProviderService.java118
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java11
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/CensorshipCircumventionFragment.java166
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java5
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java8
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/LanguageSelectionFragment.java166
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java25
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/ObfuscationProxyDialog.java41
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/SettingsFragment.java123
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java14
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Introducer.java132
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java518
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java157
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/BitmaskCoreProvider.java28
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/BuildConfigHelper.java14
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java57
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/CredentialsParser.java58
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java239
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/PrivateKeyHelper.java124
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/RSAHelper.java72
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java6
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/EIP.java29
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java9
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/Gateway.java221
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java398
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java219
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsvpnClient.java133
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/HoppingConfig.java17
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/KcpConfig.java24
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/ObfsvpnConfig.java4
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/QuicConfig.java24
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/IProviderApiManager.java11
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java30
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiEventSender.java180
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManager.java167
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java1024
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerFactory.java25
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerV3.java765
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerV5.java354
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiSetupBroadcastReceiver.java8
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiTorHandler.java54
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java23
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java6
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupObservable.java3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/SetupViewPagerAdapter.java13
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SetupActivity.java68
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/CircumventionSetupFragment.java8
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ConfigureProviderFragment.java59
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/EmptyPermissionSetupFragment.java3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/NotificationSetupFragment.java35
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/PermissionExplanationFragment.java72
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/ProviderSelectionFragment.java98
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/SetupFragmentFactory.java20
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/VpnPermissionSetupFragment.java36
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/helpers/AbstractQrScannerHelper.java16
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/fragments/viewmodel/ProviderSelectionViewModel.java61
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/TorServiceCommand.java15
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java10
-rw-r--r--app/src/main/res/drawable/bridge_automatic.xml10
-rw-r--r--app/src/main/res/drawable/bridge_manual.xml10
-rw-r--r--app/src/main/res/drawable/qr_code_scanner.xml5
-rw-r--r--app/src/main/res/drawable/translate.xml5
-rw-r--r--app/src/main/res/layout/d_obfuscation_proxy.xml32
-rw-r--r--app/src/main/res/layout/f_censorship_circumvention.xml70
-rw-r--r--app/src/main/res/layout/f_drawer_main.xml8
-rw-r--r--app/src/main/res/layout/f_language_selection.xml17
-rw-r--r--app/src/main/res/layout/f_notification_setup.xml34
-rw-r--r--app/src/main/res/layout/f_permission_explanation.xml (renamed from app/src/main/res/layout/f_vpn_permission_setup.xml)26
-rw-r--r--app/src/main/res/layout/f_provider_selection.xml88
-rw-r--r--app/src/main/res/layout/f_settings.xml83
-rw-r--r--app/src/main/res/layout/v_select_text_list_item.xml3
-rw-r--r--app/src/main/res/resources.properties1
-rw-r--r--app/src/main/res/values-ar/strings.xml30
-rw-r--r--app/src/main/res/values-cs/strings.xml208
-rw-r--r--app/src/main/res/values-de/strings.xml39
-rw-r--r--app/src/main/res/values-el/strings.xml13
-rw-r--r--app/src/main/res/values-es-rAR/strings.xml1
-rw-r--r--app/src/main/res/values-es-rCU/strings.xml6
-rw-r--r--app/src/main/res/values-es/strings.xml8
-rw-r--r--app/src/main/res/values-fi/strings.xml4
-rw-r--r--app/src/main/res/values-fr/strings.xml44
-rw-r--r--app/src/main/res/values-he/strings.xml3
-rw-r--r--app/src/main/res/values-hu/strings.xml4
-rw-r--r--app/src/main/res/values-ja/strings.xml84
-rw-r--r--app/src/main/res/values-lt/strings.xml27
-rw-r--r--app/src/main/res/values-nl/strings.xml17
-rw-r--r--app/src/main/res/values-pt-rBR/strings.xml35
-rw-r--r--app/src/main/res/values-ru/strings.xml62
-rw-r--r--app/src/main/res/values-tr/strings.xml35
-rw-r--r--app/src/main/res/values-uk/strings.xml145
-rw-r--r--app/src/main/res/values-vi/strings.xml3
-rw-r--r--app/src/main/res/values-zh-rTW/strings.xml30
-rw-r--r--app/src/main/res/values-zh/strings.xml12
-rw-r--r--app/src/main/res/values/colors.xml1
-rw-r--r--app/src/main/res/values/strings.xml35
-rw-r--r--app/src/main/res/values/untranslatable.xml28
-rw-r--r--app/src/main/res/xml/locales_config.xml26
-rw-r--r--app/src/normal/AndroidManifest.xml6
-rw-r--r--app/src/normal/assets/riseup.net.json52
-rw-r--r--app/src/normal/java/se/leap/bitmaskclient/providersetup/helpers/QrScannerHelper.java39
-rw-r--r--app/src/normalProductionFatDebug/assets/demo.bitmask.net.json37
-rw-r--r--app/src/normalProductionFatDebug/assets/demo.bitmask.net.pem10
-rw-r--r--app/src/normalProductionFatDebug/assets/urls/demo.bitmask.net.url4
-rw-r--r--app/src/production/java/se/leap/bitmaskclient/providersetup/ProviderApiManager.java418
-rw-r--r--app/src/test/java/de/blinkt/openvpn/VpnProfileTest.java54
-rw-r--r--app/src/test/java/io/swagger/client/JSONTest.java31
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/base/models/GatewayJsonTest.java2
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/base/models/IntroducerTest.java83
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/base/models/ProviderTest.java37
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/base/utils/ConfigHelperTest.java31
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/base/utils/CredentialsParserTest.java57
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/base/utils/PreferenceHelperTest.java17
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/base/utils/PrivateKeyHelperTest.java48
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/eip/GatewayTest.java159
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java152
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java487
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/pluggableTransports/models/KcpConfigTest.java15
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerTest.java276
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerV3Test.java (renamed from app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java)499
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/providersetup/ProviderManagerTest.java19
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java35
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java80
-rw-r--r--app/src/test/resources/ed25519_credentials.pem31
-rw-r--r--app/src/test/resources/multiple_pts_per_host_eip-service.json145
-rw-r--r--app/src/test/resources/private_PKCS8_encoded_ecdsa_key.pem8
-rw-r--r--app/src/test/resources/private_PKCS8_encoded_ed25519_key.pem3
146 files changed, 9736 insertions, 3322 deletions
diff --git a/app/src/custom/assets/riseup.net.json b/app/src/custom/assets/riseup.net.json
index 5e14abc3..407e2e22 100644
--- a/app/src/custom/assets/riseup.net.json
+++ b/app/src/custom/assets/riseup.net.json
@@ -1,37 +1,37 @@
{
- "api_uri":"https://api.black.riseup.net:4430",
- "api_version":"3",
- "ca_cert_fingerprint":"SHA256: dd919b7513b4a1368faa20e38cd3314156805677f48b787cdd9b4a92dec64eb0",
- "ca_cert_uri":"https://black.riseup.net/ca.crt",
- "default_language":"en",
- "description":{
- "en": "Riseup is a non-profit collective in Seattle that provides online communication tools for people and groups working toward liberatory social change."
+ "api_uri": "https://api.black.riseup.net:4430",
+ "api_version": "3",
+ "ca_cert_fingerprint": "SHA256: dd919b7513b4a1368faa20e38cd3314156805677f48b787cdd9b4a92dec64eb0",
+ "ca_cert_uri": "https://black.riseup.net/ca.crt",
+ "default_language": "en",
+ "description": {
+ "en": "Riseup Networks"
},
- "domain":"riseup.net",
- "enrollment_policy":"open",
- "languages":[
+ "domain": "riseup.net",
+ "enrollment_policy": "open",
+ "languages": [
"en"
],
- "name":{
- "en":"Riseup Networks"
+ "name": {
+ "en": "Riseup Networks"
},
- "service":{
- "allow_anonymous":true,
- "allow_free":true,
- "allow_limited_bandwidth":false,
- "allow_paid":false,
- "allow_registration":false,
- "allow_unlimited_bandwidth":true,
- "bandwidth_limit":102400,
- "default_service_level":1,
- "levels":{
- "1":{
- "description":"Please donate.",
- "name":"free"
+ "service": {
+ "allow_anonymous": true,
+ "allow_free": true,
+ "allow_limited_bandwidth": false,
+ "allow_paid": false,
+ "allow_registration": false,
+ "allow_unlimited_bandwidth": true,
+ "bandwidth_limit": 102400,
+ "default_service_level": 1,
+ "levels": {
+ "1": {
+ "description": "Please donate.",
+ "name": "free"
}
}
},
- "services":[
+ "services": [
"openvpn"
]
} \ No newline at end of file
diff --git a/app/src/custom/java/se/leap/bitmaskclient/providersetup/helpers/QrScannerHelper.java b/app/src/custom/java/se/leap/bitmaskclient/providersetup/helpers/QrScannerHelper.java
new file mode 100644
index 00000000..8483549a
--- /dev/null
+++ b/app/src/custom/java/se/leap/bitmaskclient/providersetup/helpers/QrScannerHelper.java
@@ -0,0 +1,16 @@
+package se.leap.bitmaskclient.providersetup.helpers;
+
+import androidx.fragment.app.Fragment;
+
+import se.leap.bitmaskclient.providersetup.fragments.helpers.AbstractQrScannerHelper;
+
+public class QrScannerHelper extends AbstractQrScannerHelper {
+
+ public QrScannerHelper(Fragment fragment, ScanResultCallback callback) {
+ super(fragment, callback);
+ }
+
+ @Override
+ public void startScan() {
+ }
+}
diff --git a/app/src/custom/res/values-ar/strings.xml b/app/src/custom/res/values-ar/strings.xml
index 7a5f0dae..959e8787 100644
--- a/app/src/custom/res/values-ar/strings.xml
+++ b/app/src/custom/res/values-ar/strings.xml
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
- <string name="donate_message">RiseupVPN هي شبكة خاصة افتراضية سهلة، آمنة من riseup.net. خدمة RiseupVPN لا تحتاج إلى حساب مستخدم، لا تقم بحفظ السجلات أو تتبعك بأي وسيلة. هذه الخدمة مدفوعة بالكامل بواسطة التبرعات من قبل مستخدمين مثلك. من فضلك تبرع من هنا https://riseup.net/vpn/donate. </string>
- <string name="terms_of_service">باستخدامك لهذا التطبيق فأنت توافق على شروط الخدمة المتاحة على https://riseup.net/tos. هذه الخدمة متوفرة كما هي، بدون أي ضمان، وهي تستهدف الأشخاص الذين يعملون لجعل العالم مكانًا أفضل.</string>
+ <string name="donate_message">RiseupVPN شبكة خاصة افتراضية سهلة، آمنة من riseup.net. خدمة RiseupVPN لا تحتاج إلى حساب مستخدم، لا تحفظ السجلات أو تتبعك بأي وسيلة. هذه الخدمة مدفوعة بالكامل بواسطة التبرعات من قبل مستخدمين مثلك. من فضلك تبرع من هنا https://riseup.net/vpn/donate. </string>
+ <string name="terms_of_service">باستخدامك لهذا التطبيق فأنت توافق على شروط الخدمة المتاحة على https://riseup.net/tos. هذه الخدمة متوفرة كما هي، بدون أي ضمان، وهي تتوجه لمن يعملون لجعل العالم مكانًا أفضل.</string>
</resources>
diff --git a/app/src/custom/res/values-cs/strings.xml b/app/src/custom/res/values-cs/strings.xml
new file mode 100644
index 00000000..ea653493
--- /dev/null
+++ b/app/src/custom/res/values-cs/strings.xml
@@ -0,0 +1,5 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<resources>
+ <string name="donate_message">RiseupVPN je jednoduchá, rychlá a bezpečná VPN služba od riseup.net. RiseupVPN nevyžaduje uživatelský účet, neukládá protokoly ani vás žádným způsobem nesleduje. Služba je placená výhradně z darů od uživatelů, jako jste vy. Přispějte prosím na https://riseup.net/vpn/donate. </string>
+ <string name="terms_of_service">Používáním této aplikace souhlasíte se Smluvními podmínkami dostupnými na https://riseup.net/tos. Tato služba je poskytována tak, jak je, bez záruky a je určena lidem, kteří pracují na tom, aby byl svět lepší.</string>
+</resources>
diff --git a/app/src/custom/res/values-it/strings.xml b/app/src/custom/res/values-it/strings.xml
index 385eb7e7..64b518bc 100644
--- a/app/src/custom/res/values-it/strings.xml
+++ b/app/src/custom/res/values-it/strings.xml
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
- <string name="donate_message">RiseupVPN è un servizio facile, veloce e sicuro di VPN da riseup.net. RiseupVPN non richiede un account utente, non tiene traccia e non ti registra in alcun modo. Questo servizio è pagato interamente dalle donazioni di utenti come te. Per favore dona a https://riseup.net/vpn/donate.</string>
- <string name="terms_of_service">Utilizzando questa applicazione aderisci alle Condizioni di Utilizzo disponibili a https://riseup.net/tos. Questo servizio viene fornito così com\'è, senza nessuna garanzia ed è inteso per persone che si impegnano a rendere il mondo un posto migliore.</string>
+ <string name="donate_message">RiseupVPN è un servizio VPN facile, veloce e sicuro offerto da riseup.net. RiseupVPN non richiede un account utente, non tiene registri e non traccia l\'utente in alcun modo. Questo servizio è interamente finanziato dalle donazioni di utenti come voi. Effettuate una donazione su https://riseup.net/vpn/donate. </string>
+ <string name="terms_of_service">Utilizzando questa applicazione accetti le Condizioni di Utilizzo disponibili a https://riseup.net/tos. Questo servizio viene fornito così com\'è, senza alcuna garanzia ed è destinato a persone che lavorano per rendere il mondo un posto migliore.</string>
</resources>
diff --git a/app/src/custom/res/values-uk/strings.xml b/app/src/custom/res/values-uk/strings.xml
new file mode 100644
index 00000000..5fed6150
--- /dev/null
+++ b/app/src/custom/res/values-uk/strings.xml
@@ -0,0 +1,5 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<resources>
+ <string name="donate_message">RiseupVPN – це проста, швидка та безпечна служба VPN від riseup.net. RiseupVPN не вимагає облікового запису користувача, не веде журнали та не відстежує вас у будь-який спосіб. Служба повністю оплачується коштом пожертв таких користувачів, як ви. Зробіть пожертвування на сторінці https://riseup.net/vpn/donate.</string>
+ <string name="terms_of_service">Користуючись цією програмою, ви погоджуєтеся з Умовами надання послуг, доступними за адресою https://riseup.net/tos. Служба надається як є, без жодних гарантій, та призначена для людей, які працюють, щоб зробити світ кращим.</string>
+</resources>
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8cf2e6f4..254f38d6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -31,13 +31,29 @@
android:extractNativeLibs="true"
android:appCategory="productivity"
android:logo="@mipmap/ic_launcher"
- android:theme="@style/BitmaskTheme">
+ android:theme="@style/BitmaskTheme"
+ android:localeConfig="@xml/locales_config">
<activity
android:name=".providersetup.activities.SetupActivity"
android:launchMode="singleInstance"
android:screenOrientation="portrait"
android:exported="false" />
+ <activity-alias
+ android:exported="true"
+ android:name=".InviteCodeActivity"
+ android:targetActivity=".providersetup.activities.SetupActivity">
+
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+
+ <data android:scheme="obfsvpnintro" />
+ </intent-filter>
+ </activity-alias>
+
<service
android:name="de.blinkt.openvpn.core.OpenVPNService"
android:exported="false"
@@ -95,6 +111,11 @@
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
</activity>
+
+ <activity android:name="com.journeyapps.barcodescanner.CaptureActivity"
+ android:screenOrientation="portrait"
+ tools:replace="screenOrientation" />
+
<activity
android:name=".base.MainActivity"
android:label="@string/app_name"
@@ -121,6 +142,15 @@
android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="false" />
</service>
+ <service
+ android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
+ android:enabled="false"
+ android:exported="false">
+ <meta-data
+ android:name="autoStoreLocales"
+ android:value="true" />
+ </service>
+
</application>
</manifest> \ No newline at end of file
diff --git a/app/src/main/java/de/blinkt/openvpn/VpnProfile.java b/app/src/main/java/de/blinkt/openvpn/VpnProfile.java
index 9da1e452..9e71939b 100644
--- a/app/src/main/java/de/blinkt/openvpn/VpnProfile.java
+++ b/app/src/main/java/de/blinkt/openvpn/VpnProfile.java
@@ -5,6 +5,8 @@
package de.blinkt.openvpn;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_HOP;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PROFILE;
import static se.leap.bitmaskclient.base.utils.ConfigHelper.stringEqual;
@@ -41,6 +43,7 @@ import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
@@ -50,6 +53,7 @@ import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
+import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
@@ -73,10 +77,12 @@ import de.blinkt.openvpn.core.VpnStatus;
import de.blinkt.openvpn.core.X509Utils;
import de.blinkt.openvpn.core.connection.Connection;
import de.blinkt.openvpn.core.connection.ConnectionAdapter;
+import de.blinkt.openvpn.core.connection.Obfs4Connection;
import se.leap.bitmaskclient.BuildConfig;
import se.leap.bitmaskclient.R;
import se.leap.bitmaskclient.base.models.ProviderObservable;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import se.leap.bitmaskclient.pluggableTransports.models.Obfs4Options;
public class VpnProfile implements Serializable, Cloneable {
// Note that this class cannot be moved to core where it belongs since
@@ -272,11 +278,20 @@ public class VpnProfile implements Serializable, Cloneable {
}
@Override
+ public int hashCode() {
+ int result =(mGatewayIp != null ? mGatewayIp.hashCode() : 0);
+ result = 31 * result + Arrays.hashCode(mConnections);
+ result = 31 * result + mTransportType;
+ return result;
+ }
+
+ @Override
public boolean equals(Object obj) {
if (obj instanceof VpnProfile) {
VpnProfile vp = (VpnProfile) obj;
return stringEqual(vp.mGatewayIp, mGatewayIp) &&
- vp.mTransportType == mTransportType;
+ vp.mTransportType == mTransportType &&
+ Arrays.equals(mConnections, vp.mConnections);
}
return false;
}
@@ -315,6 +330,22 @@ public class VpnProfile implements Serializable, Cloneable {
return Connection.TransportType.fromInt(mTransportType);
}
+ public @Nullable Obfs4Options getObfs4Options() {
+ Connection.TransportType transportType = getTransportType();
+ if (!(transportType == OBFS4 || transportType == OBFS4_HOP)) {
+ return null;
+ }
+ return ((Obfs4Connection) mConnections[0]).getObfs4Options();
+ }
+
+ public String getObfuscationTransportLayerProtocol() {
+ try {
+ return getObfs4Options().transport.getProtocols()[0];
+ } catch (NullPointerException | ArrayIndexOutOfBoundsException ignore) {
+ return null;
+ }
+ }
+
public String getName() {
if (TextUtils.isEmpty(mName))
return "No profile name";
@@ -444,8 +475,12 @@ public class VpnProfile implements Serializable, Cloneable {
// Client Cert + Key
cfg.append(insertFileData("cert", mClientCertFilename));
- mPrivateKey = ProviderObservable.getInstance().getCurrentProvider().getRSAPrivateKey();
- cfg.append("management-external-key nopadding pkcs1 pss digest\n");
+ mPrivateKey = ProviderObservable.getInstance().getCurrentProvider().getPrivateKey();
+ if (mPrivateKey.getAlgorithm().equalsIgnoreCase("RSA")) {
+ cfg.append("management-external-key nopadding pkcs1 pss digest\n");
+ } else {
+ cfg.append("management-external-key\n");
+ }
break;
case VpnProfile.TYPE_USERPASS_PKCS12:
@@ -1250,7 +1285,9 @@ public class VpnProfile implements Serializable, Cloneable {
return signed_bytes;
}
} catch
- (NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException | SignatureException | InvalidAlgorithmParameterException
+ (NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException |
+ BadPaddingException | NoSuchPaddingException | SignatureException |
+ InvalidAlgorithmParameterException | NoSuchProviderException
e) {
VpnStatus.logError(R.string.error_rsa_sign, e.getClass().toString(), e.getLocalizedMessage());
return null;
@@ -1296,11 +1333,13 @@ public class VpnProfile implements Serializable, Cloneable {
return hashtype;
}
- private byte[] doDigestSign(PrivateKey privkey, byte[] data, OpenVPNManagement.SignaturePadding padding, String hashalg, String saltlen) throws SignatureException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
+ private byte[] doDigestSign(PrivateKey privkey, byte[] data, OpenVPNManagement.SignaturePadding padding, String hashalg, String saltlen) throws SignatureException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchProviderException {
/* RSA */
Signature sig = null;
- if (privkey.getAlgorithm().equals("EC")) {
+ if (privkey.getAlgorithm().equals("Ed25519")) {
+ sig = Signature.getInstance("Ed25519", "BC");
+ } else if (privkey.getAlgorithm().equals("EC")) {
if (hashalg.equals(""))
hashalg = "NONE";
/* e.g. SHA512withECDSA */
diff --git a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java
index c8ac965f..a82a87d9 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNService.java
@@ -182,6 +182,7 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
synchronized (mProcessLock) {
mProcessThread = null;
}
+ stopObfsvpn();
VpnStatus.removeByteCountListener(this);
unregisterDeviceStateReceiver(mDeviceStateReceiver);
mDeviceStateReceiver = null;
@@ -230,15 +231,20 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
mDeviceStateReceiver.userPause(shouldBePaused);
}
+ private boolean stopObfsvpn() {
+ if (obfsVpnClient == null || !obfsVpnClient.isStarted()) {
+ return true;
+ }
+ boolean success = obfsVpnClient.stop();
+ obfsVpnClient = null;
+ return success;
+ }
@Override
public boolean stopVPN(boolean replaceConnection) {
+ stopObfsvpn();
if(isVpnRunning()) {
if (getManagement() != null && getManagement().stopVPN(replaceConnection)) {
if (!replaceConnection) {
- if (obfsVpnClient != null && obfsVpnClient.isStarted()) {
- obfsVpnClient.stop();
- obfsVpnClient = null;
- }
VpnStatus.updateStateString("NOPROCESS", "VPN STOPPED", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED);
}
return true;
@@ -369,24 +375,53 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
}
private void startOpenVPN() {
- //TODO: investigate how connections[n] with n>0 get called during vpn setup (on connection refused?)
- // Do we need to check if there's any obfs4 connection in mProfile.mConnections and start
- // the dispatcher here? Can we start the dispatcher at a later point of execution, e.g. when
- // connections[n], n>0 gets choosen?
-
Connection connection = mProfile.mConnections[0];
VpnStatus.setCurrentlyConnectingProfile(mProfile);
+ // stop old running obfsvpn client
+ if (!stopObfsvpn()) {
+ VpnStatus.logError("Failed to stop already running obfsvpn client");
+ endVpnService();
+ VpnStatus.updateStateString("NOPROCESS", "VPN STOPPED", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED);
+ return;
+ }
+
+ // Set a flag that we are starting a new VPN
+ mStarting = true;
+ // Stop the previous session by interrupting the thread.
+ stopOldOpenVPNProcess();
+ // An old running VPN should now be exited
+ mStarting = false;
+
+ // optionally start start obfsvpn and adapt openvpn config to the port obfsvpn is listening to
+ Connection.TransportType transportType = connection.getTransportType();
+ if (mProfile.usePluggableTransports() && transportType.isPluggableTransport()) {
+ try {
+ obfsVpnClient = new ObfsvpnClient(((Obfs4Connection) connection).getObfs4Options());
+ obfsVpnClient.start();
+ int port = obfsVpnClient.getPort();
+ connection.setServerPort(String.valueOf(port));
+ } catch (RuntimeException e) {
+ e.printStackTrace();
+ VpnStatus.logException(e);
+ endVpnService();
+ VpnStatus.updateStateString("NOPROCESS", "VPN STOPPED", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED);
+ return;
+ }
+ }
+
+ // write openvpn config
VpnStatus.logInfo(R.string.building_configration);
VpnStatus.updateStateString("VPN_GENERATE_CONFIG", "", R.string.building_configration, ConnectionStatus.LEVEL_START);
-
try {
mProfile.writeConfigFile(this);
} catch (IOException e) {
VpnStatus.logException("Error writing config file", e);
endVpnService();
+ VpnStatus.updateStateString("NOPROCESS", "VPN STOPPED", R.string.state_noprocess, ConnectionStatus.LEVEL_NOTCONNECTED);
return;
}
+
String nativeLibraryDirectory = getApplicationInfo().nativeLibraryDir;
String tmpDir;
try {
@@ -399,25 +434,6 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
// Write OpenVPN binary
String[] argv = VPNLaunchHelper.buildOpenvpnArgv(this);
-
- // Set a flag that we are starting a new VPN
- mStarting = true;
- // Stop the previous session by interrupting the thread.
-
- stopOldOpenVPNProcess();
- // An old running VPN should now be exited
- mStarting = false;
- Connection.TransportType transportType = connection.getTransportType();
- if (mProfile.usePluggableTransports() && transportType.isPluggableTransport()) {
- if (obfsVpnClient != null && obfsVpnClient.isStarted()) {
- obfsVpnClient.stop();
- }
- obfsVpnClient = new ObfsvpnClient(((Obfs4Connection) connection).getObfs4Options());
- obfsVpnClient.start();
- Log.d(TAG, "obfsvpn client started");
- }
-
-
// Start a new session by creating a new thread.
boolean useOpenVPN3 = VpnProfile.doUseOpenVPN3(this);
@@ -471,11 +487,6 @@ public class OpenVPNService extends VpnService implements StateListener, Callbac
if (mOpenVPNThread != null)
((OpenVPNThread) mOpenVPNThread).setReplaceConnection();
if (mManagement.stopVPN(true)) {
- if (obfsVpnClient != null && obfsVpnClient.isStarted()) {
- Log.d(TAG, "-> stop obfsvpnClient");
- obfsVpnClient.stop();
- obfsVpnClient = null;
- }
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
diff --git a/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java b/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java
index 88b933eb..a4b5e3be 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVpnManagementThread.java
@@ -272,12 +272,13 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement {
}
private void processCommand(String command) {
- //Log.i(TAG, "Line from managment" + command);
+ Log.i(TAG, "Line from managment " + command);
if (command.startsWith(">") && command.contains(":")) {
String[] parts = command.split(":", 2);
String cmd = parts[0].substring(1);
String argument = parts[1];
+ Log.d(">>>>", "CMD: "+ cmd + "argument: " + argument);
switch (cmd) {
@@ -735,7 +736,7 @@ public class OpenVpnManagementThread implements Runnable, OpenVPNManagement {
String[] arguments = argument.split(",");
// NC9t8IkYrjAQcCzc85zN0H5TvwfAUDwYkR4j2ga6fGw=,RSA_PKCS1_PSS_PADDING,hashalg=SHA256,saltlen=digest
-
+ // ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFRMUyAxLjMsIGNsaWVudCBIoXJ0aWZpY2F0ZVZlcmlmeQCvvTk69HvSHUhM27ghCCSgzHds1Bdsm4MyVGxlgDIJbnDj+G5Y1YxXajqy6E/G1GA=,ED25519,data=message
SignaturePadding padding = SignaturePadding.NO_PADDING;
String saltlen="";
diff --git a/app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java b/app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java
index 0b28cbca..6cd86105 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/connection/Connection.java
@@ -9,10 +9,13 @@ import static de.blinkt.openvpn.core.connection.Connection.TransportType.*;
import android.text.TextUtils;
+import androidx.annotation.NonNull;
+
import com.google.gson.annotations.JsonAdapter;
import java.io.Serializable;
import java.util.Locale;
+import java.util.Objects;
@JsonAdapter(ConnectionAdapter.class)
public abstract class Connection implements Serializable, Cloneable {
@@ -42,7 +45,8 @@ public abstract class Connection implements Serializable, Cloneable {
public enum TransportProtocol {
UDP("udp"),
TCP("tcp"),
- KCP("kcp");
+ KCP("kcp"),
+ QUIC("quic");
final String protocol;
@@ -301,5 +305,54 @@ public abstract class Connection implements Serializable, Cloneable {
this.mProxyAuthPassword = proxyAuthPassword;
}
- public abstract TransportType getTransportType();
+ public abstract @NonNull TransportType getTransportType();
+
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Connection that)) return false;
+
+ if (mUseUdp != that.mUseUdp) return false;
+ if (mUseCustomConfig != that.mUseCustomConfig) return false;
+ if (mEnabled != that.mEnabled) return false;
+ if (mConnectTimeout != that.mConnectTimeout) return false;
+ if (mUseProxyAuth != that.mUseProxyAuth) return false;
+ if (!Objects.equals(mServerName, that.mServerName))
+ return false;
+ if (!Objects.equals(mServerPort, that.mServerPort))
+ return false;
+ if (!Objects.equals(mCustomConfiguration, that.mCustomConfiguration))
+ return false;
+ if (mProxyType != that.mProxyType) return false;
+ if (!Objects.equals(mProxyName, that.mProxyName))
+ return false;
+ if (!Objects.equals(mProxyPort, that.mProxyPort))
+ return false;
+ if (!Objects.equals(mProxyAuthUser, that.mProxyAuthUser))
+ return false;
+ if (getTransportType() != that.getTransportType()) {
+ return false;
+ }
+ return Objects.equals(mProxyAuthPassword, that.mProxyAuthPassword);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mServerName != null ? mServerName.hashCode() : 0;
+ result = 31 * result + (mServerPort != null ? mServerPort.hashCode() : 0);
+ result = 31 * result + (mUseUdp ? 1 : 0);
+ result = 31 * result + (mCustomConfiguration != null ? mCustomConfiguration.hashCode() : 0);
+ result = 31 * result + (mUseCustomConfig ? 1 : 0);
+ result = 31 * result + (mEnabled ? 1 : 0);
+ result = 31 * result + mConnectTimeout;
+ result = 31 * result + (mProxyType != null ? mProxyType.hashCode() : 0);
+ result = 31 * result + (mProxyName != null ? mProxyName.hashCode() : 0);
+ result = 31 * result + (mProxyPort != null ? mProxyPort.hashCode() : 0);
+ result = 31 * result + (mUseProxyAuth ? 1 : 0);
+ result = 31 * result + (mProxyAuthUser != null ? mProxyAuthUser.hashCode() : 0);
+ result = 31 * result + (mProxyAuthPassword != null ? mProxyAuthPassword.hashCode() : 0);
+ result = 31 * result + getTransportType().toInt();
+ return result;
+ }
}
diff --git a/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java b/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java
index 0fe6bff2..e2c596ac 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/connection/Obfs4Connection.java
@@ -15,7 +15,7 @@ public class Obfs4Connection extends Connection {
public Obfs4Connection(Obfs4Options options) {
setServerName(ObfsvpnClient.IP);
- setServerPort(String.valueOf(ObfsvpnClient.PORT));
+ setServerPort(String.valueOf(ObfsvpnClient.DEFAULT_PORT));
setUseUdp(true);
setProxyType(ProxyType.NONE);
setProxyName("");
diff --git a/app/src/main/java/io/swagger/client/JSON.java b/app/src/main/java/io/swagger/client/JSON.java
new file mode 100644
index 00000000..b1ca69a8
--- /dev/null
+++ b/app/src/main/java/io/swagger/client/JSON.java
@@ -0,0 +1,399 @@
+/*
+ * Menshen API
+ * This is a LEAP VPN Service API
+ *
+ * OpenAPI spec version: 0.5.2
+ *
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+
+
+package io.swagger.client;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonParseException;
+import com.google.gson.TypeAdapter;
+import com.google.gson.internal.bind.util.ISO8601Utils;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import com.google.gson.JsonElement;
+import io.gsonfire.GsonFireBuilder;
+import io.gsonfire.TypeSelector;
+import org.threeten.bp.LocalDate;
+import org.threeten.bp.OffsetDateTime;
+import org.threeten.bp.format.DateTimeFormatter;
+
+import io.swagger.client.model.*;
+import io.swagger.client.model.*;
+import io.swagger.client.model.*;
+import io.swagger.client.model.*;
+import io.swagger.client.model.*;
+import io.swagger.client.model.*;
+import okio.ByteString;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.reflect.Type;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.util.Date;
+import java.util.Map;
+import java.util.HashMap;
+
+public class JSON {
+ private Gson gson;
+ private boolean isLenientOnJson = false;
+ private DateTypeAdapter dateTypeAdapter = new DateTypeAdapter();
+ private SqlDateTypeAdapter sqlDateTypeAdapter = new SqlDateTypeAdapter();
+ private OffsetDateTimeTypeAdapter offsetDateTimeTypeAdapter = new OffsetDateTimeTypeAdapter();
+ private LocalDateTypeAdapter localDateTypeAdapter = new LocalDateTypeAdapter();
+ private ByteArrayAdapter byteArrayAdapter = new ByteArrayAdapter();
+
+ public static GsonBuilder createGson() {
+ GsonFireBuilder fireBuilder = new GsonFireBuilder()
+ ;
+ GsonBuilder builder = fireBuilder.createGsonBuilder();
+ return builder;
+ }
+
+ private static String getDiscriminatorValue(JsonElement readElement, String discriminatorField) {
+ JsonElement element = readElement.getAsJsonObject().get(discriminatorField);
+ if(null == element) {
+ throw new IllegalArgumentException("missing discriminator field: <" + discriminatorField + ">");
+ }
+ return element.getAsString();
+ }
+
+ private static Class getClassByDiscriminator(Map classByDiscriminatorValue, String discriminatorValue) {
+ Class clazz = (Class) classByDiscriminatorValue.get(discriminatorValue.toUpperCase());
+ if(null == clazz) {
+ throw new IllegalArgumentException("cannot determine model class of name: <" + discriminatorValue + ">");
+ }
+ return clazz;
+ }
+
+ public JSON() {
+ gson = createGson()
+ .registerTypeAdapter(Date.class, dateTypeAdapter)
+ .registerTypeAdapter(java.sql.Date.class, sqlDateTypeAdapter)
+ .registerTypeAdapter(OffsetDateTime.class, offsetDateTimeTypeAdapter)
+ .registerTypeAdapter(LocalDate.class, localDateTypeAdapter)
+ .registerTypeAdapter(byte[].class, byteArrayAdapter)
+ .create();
+ }
+
+ /**
+ * Get Gson.
+ *
+ * @return Gson
+ */
+ public Gson getGson() {
+ return gson;
+ }
+
+ /**
+ * Set Gson.
+ *
+ * @param gson Gson
+ * @return JSON
+ */
+ public JSON setGson(Gson gson) {
+ this.gson = gson;
+ return this;
+ }
+
+ public JSON setLenientOnJson(boolean lenientOnJson) {
+ isLenientOnJson = lenientOnJson;
+ return this;
+ }
+
+ /**
+ * Serialize the given Java object into JSON string.
+ *
+ * @param obj Object
+ * @return String representation of the JSON
+ */
+ public String serialize(Object obj) {
+ return gson.toJson(obj);
+ }
+
+ /**
+ * Deserialize the given JSON string to Java object.
+ *
+ * @param <T> Type
+ * @param body The JSON string
+ * @param returnType The type to deserialize into
+ * @return The deserialized Java object
+ */
+ @SuppressWarnings("unchecked")
+ public <T> T deserialize(String body, Type returnType) {
+ try {
+ if (isLenientOnJson) {
+ JsonReader jsonReader = new JsonReader(new StringReader(body));
+ // see https://google-gson.googlecode.com/svn/trunk/gson/docs/javadocs/com/google/gson/stream/JsonReader.html#setLenient(boolean)
+ jsonReader.setLenient(true);
+ return gson.fromJson(jsonReader, returnType);
+ } else {
+ return gson.fromJson(body, returnType);
+ }
+ } catch (JsonParseException e) {
+ // Fallback processing when failed to parse JSON form response body:
+ // return the response body string directly for the String return type;
+ if (returnType.equals(String.class))
+ return (T) body;
+ else throw (e);
+ }
+ }
+
+ /**
+ * Gson TypeAdapter for Byte Array type
+ */
+ public class ByteArrayAdapter extends TypeAdapter<byte[]> {
+
+ @Override
+ public void write(JsonWriter out, byte[] value) throws IOException {
+ if (value == null) {
+ out.nullValue();
+ } else {
+ out.value(ByteString.of(value).base64());
+ }
+ }
+
+ @Override
+ public byte[] read(JsonReader in) throws IOException {
+ switch (in.peek()) {
+ case NULL:
+ in.nextNull();
+ return null;
+ default:
+ String bytesAsBase64 = in.nextString();
+ ByteString byteString = ByteString.decodeBase64(bytesAsBase64);
+ return byteString.toByteArray();
+ }
+ }
+ }
+
+ /**
+ * Gson TypeAdapter for JSR310 OffsetDateTime type
+ */
+ public static class OffsetDateTimeTypeAdapter extends TypeAdapter<OffsetDateTime> {
+
+ private DateTimeFormatter formatter;
+
+ public OffsetDateTimeTypeAdapter() {
+ this(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+ }
+
+ public OffsetDateTimeTypeAdapter(DateTimeFormatter formatter) {
+ this.formatter = formatter;
+ }
+
+ public void setFormat(DateTimeFormatter dateFormat) {
+ this.formatter = dateFormat;
+ }
+
+ @Override
+ public void write(JsonWriter out, OffsetDateTime date) throws IOException {
+ if (date == null) {
+ out.nullValue();
+ } else {
+ out.value(formatter.format(date));
+ }
+ }
+
+ @Override
+ public OffsetDateTime read(JsonReader in) throws IOException {
+ switch (in.peek()) {
+ case NULL:
+ in.nextNull();
+ return null;
+ default:
+ String date = in.nextString();
+ if (date.endsWith("+0000")) {
+ date = date.substring(0, date.length()-5) + "Z";
+ }
+ return OffsetDateTime.parse(date, formatter);
+ }
+ }
+ }
+
+ /**
+ * Gson TypeAdapter for JSR310 LocalDate type
+ */
+ public class LocalDateTypeAdapter extends TypeAdapter<LocalDate> {
+
+ private DateTimeFormatter formatter;
+
+ public LocalDateTypeAdapter() {
+ this(DateTimeFormatter.ISO_LOCAL_DATE);
+ }
+
+ public LocalDateTypeAdapter(DateTimeFormatter formatter) {
+ this.formatter = formatter;
+ }
+
+ public void setFormat(DateTimeFormatter dateFormat) {
+ this.formatter = dateFormat;
+ }
+
+ @Override
+ public void write(JsonWriter out, LocalDate date) throws IOException {
+ if (date == null) {
+ out.nullValue();
+ } else {
+ out.value(formatter.format(date));
+ }
+ }
+
+ @Override
+ public LocalDate read(JsonReader in) throws IOException {
+ switch (in.peek()) {
+ case NULL:
+ in.nextNull();
+ return null;
+ default:
+ String date = in.nextString();
+ return LocalDate.parse(date, formatter);
+ }
+ }
+ }
+
+ public JSON setOffsetDateTimeFormat(DateTimeFormatter dateFormat) {
+ offsetDateTimeTypeAdapter.setFormat(dateFormat);
+ return this;
+ }
+
+ public JSON setLocalDateFormat(DateTimeFormatter dateFormat) {
+ localDateTypeAdapter.setFormat(dateFormat);
+ return this;
+ }
+
+ /**
+ * Gson TypeAdapter for java.sql.Date type
+ * If the dateFormat is null, a simple "yyyy-MM-dd" format will be used
+ * (more efficient than SimpleDateFormat).
+ */
+ public static class SqlDateTypeAdapter extends TypeAdapter<java.sql.Date> {
+
+ private DateFormat dateFormat;
+
+ public SqlDateTypeAdapter() {
+ }
+
+ public SqlDateTypeAdapter(DateFormat dateFormat) {
+ this.dateFormat = dateFormat;
+ }
+
+ public void setFormat(DateFormat dateFormat) {
+ this.dateFormat = dateFormat;
+ }
+
+ @Override
+ public void write(JsonWriter out, java.sql.Date date) throws IOException {
+ if (date == null) {
+ out.nullValue();
+ } else {
+ String value;
+ if (dateFormat != null) {
+ value = dateFormat.format(date);
+ } else {
+ value = date.toString();
+ }
+ out.value(value);
+ }
+ }
+
+ @Override
+ public java.sql.Date read(JsonReader in) throws IOException {
+ switch (in.peek()) {
+ case NULL:
+ in.nextNull();
+ return null;
+ default:
+ String date = in.nextString();
+ try {
+ if (dateFormat != null) {
+ return new java.sql.Date(dateFormat.parse(date).getTime());
+ }
+ return new java.sql.Date(ISO8601Utils.parse(date, new ParsePosition(0)).getTime());
+ } catch (ParseException e) {
+ throw new JsonParseException(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Gson TypeAdapter for java.util.Date type
+ * If the dateFormat is null, ISO8601Utils will be used.
+ */
+ public static class DateTypeAdapter extends TypeAdapter<Date> {
+
+ private DateFormat dateFormat;
+
+ public DateTypeAdapter() {
+ }
+
+ public DateTypeAdapter(DateFormat dateFormat) {
+ this.dateFormat = dateFormat;
+ }
+
+ public void setFormat(DateFormat dateFormat) {
+ this.dateFormat = dateFormat;
+ }
+
+ @Override
+ public void write(JsonWriter out, Date date) throws IOException {
+ if (date == null) {
+ out.nullValue();
+ } else {
+ String value;
+ if (dateFormat != null) {
+ value = dateFormat.format(date);
+ } else {
+ value = ISO8601Utils.format(date, true);
+ }
+ out.value(value);
+ }
+ }
+
+ @Override
+ public Date read(JsonReader in) throws IOException {
+ try {
+ switch (in.peek()) {
+ case NULL:
+ in.nextNull();
+ return null;
+ default:
+ String date = in.nextString();
+ try {
+ if (dateFormat != null) {
+ return dateFormat.parse(date);
+ }
+ return ISO8601Utils.parse(date, new ParsePosition(0));
+ } catch (ParseException e) {
+ throw new JsonParseException(e);
+ }
+ }
+ } catch (IllegalArgumentException e) {
+ throw new JsonParseException(e);
+ }
+ }
+ }
+
+ public JSON setDateFormat(DateFormat dateFormat) {
+ dateTypeAdapter.setFormat(dateFormat);
+ return this;
+ }
+
+ public JSON setSqlDateFormat(DateFormat dateFormat) {
+ sqlDateTypeAdapter.setFormat(dateFormat);
+ return this;
+ }
+
+}
diff --git a/app/src/main/java/io/swagger/client/model/ModelsBridge.java b/app/src/main/java/io/swagger/client/model/ModelsBridge.java
new file mode 100644
index 00000000..88f3575e
--- /dev/null
+++ b/app/src/main/java/io/swagger/client/model/ModelsBridge.java
@@ -0,0 +1,428 @@
+/*
+ * Menshen API
+ * This is a LEAP VPN Service API
+ *
+ * OpenAPI spec version: 0.5.2
+ *
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+
+
+package io.swagger.client.model;
+
+import java.util.Objects;
+import java.util.Arrays;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * ModelsBridge
+ */
+
+public class ModelsBridge {
+ @SerializedName("auth")
+ private String auth = null;
+
+ @SerializedName("bucket")
+ private String bucket = null;
+
+ @SerializedName("experimental")
+ private Boolean experimental = null;
+
+ @SerializedName("healthy")
+ private Boolean healthy = null;
+
+ @SerializedName("host")
+ private String host = null;
+
+ @SerializedName("ip_addr")
+ private String ipAddr = null;
+
+ @SerializedName("ip6_addr")
+ private String ip6Addr = null;
+
+ @SerializedName("last_seen_millis")
+ private Long lastSeenMillis = null;
+
+ @SerializedName("load")
+ private BigDecimal load = null;
+
+ @SerializedName("location")
+ private String location = null;
+
+ @SerializedName("options")
+ private Map<String, Object> options = null;
+
+ @SerializedName("overloaded")
+ private Boolean overloaded = null;
+
+ @SerializedName("port")
+ private Integer port = null;
+
+ @SerializedName("transport")
+ private String transport = null;
+
+ @SerializedName("type")
+ private String type = null;
+
+ public ModelsBridge auth(String auth) {
+ this.auth = auth;
+ return this;
+ }
+
+ /**
+ * Any authentication method needed for connect to the bridge, &#x60;none&#x60; otherwise.
+ * @return auth
+ **/
+ @ApiModelProperty(value = "Any authentication method needed for connect to the bridge, `none` otherwise.")
+ public String getAuth() {
+ return auth;
+ }
+
+ public void setAuth(String auth) {
+ this.auth = auth;
+ }
+
+ public ModelsBridge bucket(String bucket) {
+ this.bucket = bucket;
+ return this;
+ }
+
+ /**
+ * Bucket is a \&quot;bucket\&quot; tag that connotes a resource group that a user may or may not have access to. An empty bucket string implies that it is open access
+ * @return bucket
+ **/
+ @ApiModelProperty(value = "Bucket is a \"bucket\" tag that connotes a resource group that a user may or may not have access to. An empty bucket string implies that it is open access")
+ public String getBucket() {
+ return bucket;
+ }
+
+ public void setBucket(String bucket) {
+ this.bucket = bucket;
+ }
+
+ public ModelsBridge experimental(Boolean experimental) {
+ this.experimental = experimental;
+ return this;
+ }
+
+ /**
+ * An experimental bridge flags any bridge that, for whatever reason, is not deemed stable. The expectation is that clients have to opt-in to experimental bridges (and gateways too).
+ * @return experimental
+ **/
+ @ApiModelProperty(value = "An experimental bridge flags any bridge that, for whatever reason, is not deemed stable. The expectation is that clients have to opt-in to experimental bridges (and gateways too).")
+ public Boolean isExperimental() {
+ return experimental;
+ }
+
+ public void setExperimental(Boolean experimental) {
+ this.experimental = experimental;
+ }
+
+ public ModelsBridge healthy(Boolean healthy) {
+ this.healthy = healthy;
+ return this;
+ }
+
+ /**
+ * Healthy indicates whether this bridge can be used normally.
+ * @return healthy
+ **/
+ @ApiModelProperty(value = "Healthy indicates whether this bridge can be used normally.")
+ public Boolean isHealthy() {
+ return healthy;
+ }
+
+ public void setHealthy(Boolean healthy) {
+ this.healthy = healthy;
+ }
+
+ public ModelsBridge host(String host) {
+ this.host = host;
+ return this;
+ }
+
+ /**
+ * Host is a unique identifier for the bridge.
+ * @return host
+ **/
+ @ApiModelProperty(value = "Host is a unique identifier for the bridge.")
+ public String getHost() {
+ return host;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ public ModelsBridge ipAddr(String ipAddr) {
+ this.ipAddr = ipAddr;
+ return this;
+ }
+
+ /**
+ * IPAddr is the IPv4 address
+ * @return ipAddr
+ **/
+ @ApiModelProperty(value = "IPAddr is the IPv4 address")
+ public String getIpAddr() {
+ return ipAddr;
+ }
+
+ public void setIpAddr(String ipAddr) {
+ this.ipAddr = ipAddr;
+ }
+
+ public ModelsBridge ip6Addr(String ip6Addr) {
+ this.ip6Addr = ip6Addr;
+ return this;
+ }
+
+ /**
+ * IP6Addr is the IPv6 address
+ * @return ip6Addr
+ **/
+ @ApiModelProperty(value = "IP6Addr is the IPv6 address")
+ public String getIp6Addr() {
+ return ip6Addr;
+ }
+
+ public void setIp6Addr(String ip6Addr) {
+ this.ip6Addr = ip6Addr;
+ }
+
+ public ModelsBridge lastSeenMillis(Long lastSeenMillis) {
+ this.lastSeenMillis = lastSeenMillis;
+ return this;
+ }
+
+ /**
+ * LastSeenMillis is a unix time in milliseconds representing the last time we received a heartbeat update from this bridge
+ * @return lastSeenMillis
+ **/
+ @ApiModelProperty(value = "LastSeenMillis is a unix time in milliseconds representing the last time we received a heartbeat update from this bridge")
+ public Long getLastSeenMillis() {
+ return lastSeenMillis;
+ }
+
+ public void setLastSeenMillis(Long lastSeenMillis) {
+ this.lastSeenMillis = lastSeenMillis;
+ }
+
+ public ModelsBridge load(BigDecimal load) {
+ this.load = load;
+ return this;
+ }
+
+ /**
+ * Load is the fractional load - but for now menshen agent is not measuring load in the bridges.
+ * @return load
+ **/
+ @ApiModelProperty(value = "Load is the fractional load - but for now menshen agent is not measuring load in the bridges.")
+ public BigDecimal getLoad() {
+ return load;
+ }
+
+ public void setLoad(BigDecimal load) {
+ this.load = load;
+ }
+
+ public ModelsBridge location(String location) {
+ this.location = location;
+ return this;
+ }
+
+ /**
+ * Location refers to the location to which this bridge points to
+ * @return location
+ **/
+ @ApiModelProperty(value = "Location refers to the location to which this bridge points to")
+ public String getLocation() {
+ return location;
+ }
+
+ public void setLocation(String location) {
+ this.location = location;
+ }
+
+ public ModelsBridge options(Map<String, Object> options) {
+ this.options = options;
+ return this;
+ }
+
+ public ModelsBridge putOptionsItem(String key, Object optionsItem) {
+ if (this.options == null) {
+ this.options = new HashMap<String, Object>();
+ }
+ this.options.put(key, optionsItem);
+ return this;
+ }
+
+ /**
+ * Options contain the map of options that will be passed to the client. It usually contains authentication credentials.
+ * @return options
+ **/
+ @ApiModelProperty(value = "Options contain the map of options that will be passed to the client. It usually contains authentication credentials.")
+ public Map<String, Object> getOptions() {
+ return options;
+ }
+
+ public void setOptions(Map<String, Object> options) {
+ this.options = options;
+ }
+
+ public ModelsBridge overloaded(Boolean overloaded) {
+ this.overloaded = overloaded;
+ return this;
+ }
+
+ /**
+ * Overloaded should be set to true if the fractional load is above threshold.
+ * @return overloaded
+ **/
+ @ApiModelProperty(value = "Overloaded should be set to true if the fractional load is above threshold.")
+ public Boolean isOverloaded() {
+ return overloaded;
+ }
+
+ public void setOverloaded(Boolean overloaded) {
+ this.overloaded = overloaded;
+ }
+
+ public ModelsBridge port(Integer port) {
+ this.port = port;
+ return this;
+ }
+
+ /**
+ * For some protocols (like hopping) port is undefined.
+ * @return port
+ **/
+ @ApiModelProperty(value = "For some protocols (like hopping) port is undefined.")
+ public Integer getPort() {
+ return port;
+ }
+
+ public void setPort(Integer port) {
+ this.port = port;
+ }
+
+ public ModelsBridge transport(String transport) {
+ this.transport = transport;
+ return this;
+ }
+
+ /**
+ * TCP, UDP or KCP. This was called \&quot;protocol\&quot; before.
+ * @return transport
+ **/
+ @ApiModelProperty(value = "TCP, UDP or KCP. This was called \"protocol\" before.")
+ public String getTransport() {
+ return transport;
+ }
+
+ public void setTransport(String transport) {
+ this.transport = transport;
+ }
+
+ public ModelsBridge type(String type) {
+ this.type = type;
+ return this;
+ }
+
+ /**
+ * Type of bridge.
+ * @return type
+ **/
+ @ApiModelProperty(value = "Type of bridge.")
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+
+ @Override
+ public boolean equals(java.lang.Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ModelsBridge modelsBridge = (ModelsBridge) o;
+ return Objects.equals(this.auth, modelsBridge.auth) &&
+ Objects.equals(this.bucket, modelsBridge.bucket) &&
+ Objects.equals(this.experimental, modelsBridge.experimental) &&
+ Objects.equals(this.healthy, modelsBridge.healthy) &&
+ Objects.equals(this.host, modelsBridge.host) &&
+ Objects.equals(this.ipAddr, modelsBridge.ipAddr) &&
+ Objects.equals(this.ip6Addr, modelsBridge.ip6Addr) &&
+ Objects.equals(this.lastSeenMillis, modelsBridge.lastSeenMillis) &&
+ Objects.equals(this.load, modelsBridge.load) &&
+ Objects.equals(this.location, modelsBridge.location) &&
+ Objects.equals(this.options, modelsBridge.options) &&
+ Objects.equals(this.overloaded, modelsBridge.overloaded) &&
+ Objects.equals(this.port, modelsBridge.port) &&
+ Objects.equals(this.transport, modelsBridge.transport) &&
+ Objects.equals(this.type, modelsBridge.type);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(auth, bucket, experimental, healthy, host, ipAddr, ip6Addr, lastSeenMillis, load, location, options, overloaded, port, transport, type);
+ }
+
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class ModelsBridge {\n");
+
+ sb.append(" auth: ").append(toIndentedString(auth)).append("\n");
+ sb.append(" bucket: ").append(toIndentedString(bucket)).append("\n");
+ sb.append(" experimental: ").append(toIndentedString(experimental)).append("\n");
+ sb.append(" healthy: ").append(toIndentedString(healthy)).append("\n");
+ sb.append(" host: ").append(toIndentedString(host)).append("\n");
+ sb.append(" ipAddr: ").append(toIndentedString(ipAddr)).append("\n");
+ sb.append(" ip6Addr: ").append(toIndentedString(ip6Addr)).append("\n");
+ sb.append(" lastSeenMillis: ").append(toIndentedString(lastSeenMillis)).append("\n");
+ sb.append(" load: ").append(toIndentedString(load)).append("\n");
+ sb.append(" location: ").append(toIndentedString(location)).append("\n");
+ sb.append(" options: ").append(toIndentedString(options)).append("\n");
+ sb.append(" overloaded: ").append(toIndentedString(overloaded)).append("\n");
+ sb.append(" port: ").append(toIndentedString(port)).append("\n");
+ sb.append(" transport: ").append(toIndentedString(transport)).append("\n");
+ sb.append(" type: ").append(toIndentedString(type)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(java.lang.Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+
+}
+
diff --git a/app/src/main/java/io/swagger/client/model/ModelsEIPService.java b/app/src/main/java/io/swagger/client/model/ModelsEIPService.java
new file mode 100644
index 00000000..939f8aa6
--- /dev/null
+++ b/app/src/main/java/io/swagger/client/model/ModelsEIPService.java
@@ -0,0 +1,206 @@
+/*
+ * Menshen API
+ * This is a LEAP VPN Service API
+ *
+ * OpenAPI spec version: 0.5.2
+ *
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+
+
+package io.swagger.client.model;
+
+import java.util.Objects;
+import java.util.Arrays;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import io.swagger.client.model.ModelsLocation;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * ModelsEIPService
+ */
+
+public class ModelsEIPService {
+ @SerializedName("auth")
+ private String auth = null;
+
+ @SerializedName("locations")
+ private Map<String, ModelsLocation> locations = null;
+
+ @SerializedName("openvpn_configuration")
+ private Map<String, Object> openvpnConfiguration = null;
+
+ @SerializedName("serial")
+ private Integer serial = null;
+
+ @SerializedName("version")
+ private Integer version = null;
+
+ public ModelsEIPService auth(String auth) {
+ this.auth = auth;
+ return this;
+ }
+
+ /**
+ * Get auth
+ * @return auth
+ **/
+ @ApiModelProperty(value = "")
+ public String getAuth() {
+ return auth;
+ }
+
+ public void setAuth(String auth) {
+ this.auth = auth;
+ }
+
+ public ModelsEIPService locations(Map<String, ModelsLocation> locations) {
+ this.locations = locations;
+ return this;
+ }
+
+ public ModelsEIPService putLocationsItem(String key, ModelsLocation locationsItem) {
+ if (this.locations == null) {
+ this.locations = new HashMap<String, ModelsLocation>();
+ }
+ this.locations.put(key, locationsItem);
+ return this;
+ }
+
+ /**
+ * Get locations
+ * @return locations
+ **/
+ @ApiModelProperty(value = "")
+ public Map<String, ModelsLocation> getLocations() {
+ return locations;
+ }
+
+ public void setLocations(Map<String, ModelsLocation> locations) {
+ this.locations = locations;
+ }
+
+ public ModelsEIPService openvpnConfiguration(Map<String, Object> openvpnConfiguration) {
+ this.openvpnConfiguration = openvpnConfiguration;
+ return this;
+ }
+
+ public ModelsEIPService putOpenvpnConfigurationItem(String key, Object openvpnConfigurationItem) {
+ if (this.openvpnConfiguration == null) {
+ this.openvpnConfiguration = new HashMap<String, Object>();
+ }
+ this.openvpnConfiguration.put(key, openvpnConfigurationItem);
+ return this;
+ }
+
+ /**
+ * Get openvpnConfiguration
+ * @return openvpnConfiguration
+ **/
+ @ApiModelProperty(value = "")
+ public Map<String, Object> getOpenvpnConfiguration() {
+ return openvpnConfiguration;
+ }
+
+ public void setOpenvpnConfiguration(Map<String, Object> openvpnConfiguration) {
+ this.openvpnConfiguration = openvpnConfiguration;
+ }
+
+ public ModelsEIPService serial(Integer serial) {
+ this.serial = serial;
+ return this;
+ }
+
+ /**
+ * Get serial
+ * @return serial
+ **/
+ @ApiModelProperty(value = "")
+ public Integer getSerial() {
+ return serial;
+ }
+
+ public void setSerial(Integer serial) {
+ this.serial = serial;
+ }
+
+ public ModelsEIPService version(Integer version) {
+ this.version = version;
+ return this;
+ }
+
+ /**
+ * Get version
+ * @return version
+ **/
+ @ApiModelProperty(value = "")
+ public Integer getVersion() {
+ return version;
+ }
+
+ public void setVersion(Integer version) {
+ this.version = version;
+ }
+
+
+ @Override
+ public boolean equals(java.lang.Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ModelsEIPService modelsEIPService = (ModelsEIPService) o;
+ return Objects.equals(this.auth, modelsEIPService.auth) &&
+ Objects.equals(this.locations, modelsEIPService.locations) &&
+ Objects.equals(this.openvpnConfiguration, modelsEIPService.openvpnConfiguration) &&
+ Objects.equals(this.serial, modelsEIPService.serial) &&
+ Objects.equals(this.version, modelsEIPService.version);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(auth, locations, openvpnConfiguration, serial, version);
+ }
+
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class ModelsEIPService {\n");
+
+ sb.append(" auth: ").append(toIndentedString(auth)).append("\n");
+ sb.append(" locations: ").append(toIndentedString(locations)).append("\n");
+ sb.append(" openvpnConfiguration: ").append(toIndentedString(openvpnConfiguration)).append("\n");
+ sb.append(" serial: ").append(toIndentedString(serial)).append("\n");
+ sb.append(" version: ").append(toIndentedString(version)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(java.lang.Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+
+}
+
diff --git a/app/src/main/java/io/swagger/client/model/ModelsGateway.java b/app/src/main/java/io/swagger/client/model/ModelsGateway.java
new file mode 100644
index 00000000..8c020eb1
--- /dev/null
+++ b/app/src/main/java/io/swagger/client/model/ModelsGateway.java
@@ -0,0 +1,371 @@
+/*
+ * Menshen API
+ * This is a LEAP VPN Service API
+ *
+ * OpenAPI spec version: 0.5.2
+ *
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+
+
+package io.swagger.client.model;
+
+import java.util.Objects;
+import java.util.Arrays;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import java.io.IOException;
+import java.math.BigDecimal;
+
+/**
+ * ModelsGateway
+ */
+
+public class ModelsGateway {
+ @SerializedName("bucket")
+ private String bucket = null;
+
+ @SerializedName("experimental")
+ private Boolean experimental = null;
+
+ @SerializedName("healthy")
+ private Boolean healthy = null;
+
+ @SerializedName("host")
+ private String host = null;
+
+ @SerializedName("ip_addr")
+ private String ipAddr = null;
+
+ @SerializedName("ip6_addr")
+ private String ip6Addr = null;
+
+ @SerializedName("last_seen_millis")
+ private Long lastSeenMillis = null;
+
+ @SerializedName("load")
+ private BigDecimal load = null;
+
+ @SerializedName("location")
+ private String location = null;
+
+ @SerializedName("overloaded")
+ private Boolean overloaded = null;
+
+ @SerializedName("port")
+ private Integer port = null;
+
+ @SerializedName("transport")
+ private String transport = null;
+
+ @SerializedName("type")
+ private String type = null;
+
+ public ModelsGateway bucket(String bucket) {
+ this.bucket = bucket;
+ return this;
+ }
+
+ /**
+ * Bucket is a \&quot;bucket\&quot; tag that connotes a resource group that a user may or may not have access to. An empty bucket string implies that it is open access
+ * @return bucket
+ **/
+ @ApiModelProperty(value = "Bucket is a \"bucket\" tag that connotes a resource group that a user may or may not have access to. An empty bucket string implies that it is open access")
+ public String getBucket() {
+ return bucket;
+ }
+
+ public void setBucket(String bucket) {
+ this.bucket = bucket;
+ }
+
+ public ModelsGateway experimental(Boolean experimental) {
+ this.experimental = experimental;
+ return this;
+ }
+
+ /**
+ * An experimental gateway flags any gateway that, for whatever reason, is not deemed stable. The expectation is that clients have to opt-in to experimental gateways (and bridges too).
+ * @return experimental
+ **/
+ @ApiModelProperty(value = "An experimental gateway flags any gateway that, for whatever reason, is not deemed stable. The expectation is that clients have to opt-in to experimental gateways (and bridges too).")
+ public Boolean isExperimental() {
+ return experimental;
+ }
+
+ public void setExperimental(Boolean experimental) {
+ this.experimental = experimental;
+ }
+
+ public ModelsGateway healthy(Boolean healthy) {
+ this.healthy = healthy;
+ return this;
+ }
+
+ /**
+ * Not used now - we could potentially flag gateways that are planned to undergo maintenance mode some time in advance. We can also automatically flag as not healthy gateways that appear not to be routing to the internet.
+ * @return healthy
+ **/
+ @ApiModelProperty(value = "Not used now - we could potentially flag gateways that are planned to undergo maintenance mode some time in advance. We can also automatically flag as not healthy gateways that appear not to be routing to the internet.")
+ public Boolean isHealthy() {
+ return healthy;
+ }
+
+ public void setHealthy(Boolean healthy) {
+ this.healthy = healthy;
+ }
+
+ public ModelsGateway host(String host) {
+ this.host = host;
+ return this;
+ }
+
+ /**
+ * Host is a unique identifier for the gateway host. It does not need to resolve, since we&#39;re not using DNS to resolve the gateways.
+ * @return host
+ **/
+ @ApiModelProperty(value = "Host is a unique identifier for the gateway host. It does not need to resolve, since we're not using DNS to resolve the gateways.")
+ public String getHost() {
+ return host;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ public ModelsGateway ipAddr(String ipAddr) {
+ this.ipAddr = ipAddr;
+ return this;
+ }
+
+ /**
+ * IPAddr is the IPv4 address
+ * @return ipAddr
+ **/
+ @ApiModelProperty(value = "IPAddr is the IPv4 address")
+ public String getIpAddr() {
+ return ipAddr;
+ }
+
+ public void setIpAddr(String ipAddr) {
+ this.ipAddr = ipAddr;
+ }
+
+ public ModelsGateway ip6Addr(String ip6Addr) {
+ this.ip6Addr = ip6Addr;
+ return this;
+ }
+
+ /**
+ * IP6Addr is the IPv6 address
+ * @return ip6Addr
+ **/
+ @ApiModelProperty(value = "IP6Addr is the IPv6 address")
+ public String getIp6Addr() {
+ return ip6Addr;
+ }
+
+ public void setIp6Addr(String ip6Addr) {
+ this.ip6Addr = ip6Addr;
+ }
+
+ public ModelsGateway lastSeenMillis(Long lastSeenMillis) {
+ this.lastSeenMillis = lastSeenMillis;
+ return this;
+ }
+
+ /**
+ * LastSeenMillis is a unix time in milliseconds representing the last time we received a heartbeat update from this gateway
+ * @return lastSeenMillis
+ **/
+ @ApiModelProperty(value = "LastSeenMillis is a unix time in milliseconds representing the last time we received a heartbeat update from this gateway")
+ public Long getLastSeenMillis() {
+ return lastSeenMillis;
+ }
+
+ public void setLastSeenMillis(Long lastSeenMillis) {
+ this.lastSeenMillis = lastSeenMillis;
+ }
+
+ public ModelsGateway load(BigDecimal load) {
+ this.load = load;
+ return this;
+ }
+
+ /**
+ * Load is the fractional load received from the menshen agent. For the time being it is a synthethic metric that takes into account number of clients and network information for the node.
+ * @return load
+ **/
+ @ApiModelProperty(value = "Load is the fractional load received from the menshen agent. For the time being it is a synthethic metric that takes into account number of clients and network information for the node.")
+ public BigDecimal getLoad() {
+ return load;
+ }
+
+ public void setLoad(BigDecimal load) {
+ this.load = load;
+ }
+
+ public ModelsGateway location(String location) {
+ this.location = location;
+ return this;
+ }
+
+ /**
+ * Location is the canonical label for the location of the gateway.
+ * @return location
+ **/
+ @ApiModelProperty(value = "Location is the canonical label for the location of the gateway.")
+ public String getLocation() {
+ return location;
+ }
+
+ public void setLocation(String location) {
+ this.location = location;
+ }
+
+ public ModelsGateway overloaded(Boolean overloaded) {
+ this.overloaded = overloaded;
+ return this;
+ }
+
+ /**
+ * Overloaded should be set to true if the fractional load is above threshold.
+ * @return overloaded
+ **/
+ @ApiModelProperty(value = "Overloaded should be set to true if the fractional load is above threshold.")
+ public Boolean isOverloaded() {
+ return overloaded;
+ }
+
+ public void setOverloaded(Boolean overloaded) {
+ this.overloaded = overloaded;
+ }
+
+ public ModelsGateway port(Integer port) {
+ this.port = port;
+ return this;
+ }
+
+ /**
+ * The (primary) port this gateway is listening on.
+ * @return port
+ **/
+ @ApiModelProperty(value = "The (primary) port this gateway is listening on.")
+ public Integer getPort() {
+ return port;
+ }
+
+ public void setPort(Integer port) {
+ this.port = port;
+ }
+
+ public ModelsGateway transport(String transport) {
+ this.transport = transport;
+ return this;
+ }
+
+ /**
+ * TCP, UDP, KCP or Quic. This was called \&quot;protocol\&quot; in previous versions of the API.
+ * @return transport
+ **/
+ @ApiModelProperty(value = "TCP, UDP, KCP or Quic. This was called \"protocol\" in previous versions of the API.")
+ public String getTransport() {
+ return transport;
+ }
+
+ public void setTransport(String transport) {
+ this.transport = transport;
+ }
+
+ public ModelsGateway type(String type) {
+ this.type = type;
+ return this;
+ }
+
+ /**
+ * Type is the type of gateway. The only valid type as of 2023 is openvpn.
+ * @return type
+ **/
+ @ApiModelProperty(value = "Type is the type of gateway. The only valid type as of 2023 is openvpn.")
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+
+ @Override
+ public boolean equals(java.lang.Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ModelsGateway modelsGateway = (ModelsGateway) o;
+ return Objects.equals(this.bucket, modelsGateway.bucket) &&
+ Objects.equals(this.experimental, modelsGateway.experimental) &&
+ Objects.equals(this.healthy, modelsGateway.healthy) &&
+ Objects.equals(this.host, modelsGateway.host) &&
+ Objects.equals(this.ipAddr, modelsGateway.ipAddr) &&
+ Objects.equals(this.ip6Addr, modelsGateway.ip6Addr) &&
+ Objects.equals(this.lastSeenMillis, modelsGateway.lastSeenMillis) &&
+ Objects.equals(this.load, modelsGateway.load) &&
+ Objects.equals(this.location, modelsGateway.location) &&
+ Objects.equals(this.overloaded, modelsGateway.overloaded) &&
+ Objects.equals(this.port, modelsGateway.port) &&
+ Objects.equals(this.transport, modelsGateway.transport) &&
+ Objects.equals(this.type, modelsGateway.type);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(bucket, experimental, healthy, host, ipAddr, ip6Addr, lastSeenMillis, load, location, overloaded, port, transport, type);
+ }
+
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class ModelsGateway {\n");
+
+ sb.append(" bucket: ").append(toIndentedString(bucket)).append("\n");
+ sb.append(" experimental: ").append(toIndentedString(experimental)).append("\n");
+ sb.append(" healthy: ").append(toIndentedString(healthy)).append("\n");
+ sb.append(" host: ").append(toIndentedString(host)).append("\n");
+ sb.append(" ipAddr: ").append(toIndentedString(ipAddr)).append("\n");
+ sb.append(" ip6Addr: ").append(toIndentedString(ip6Addr)).append("\n");
+ sb.append(" lastSeenMillis: ").append(toIndentedString(lastSeenMillis)).append("\n");
+ sb.append(" load: ").append(toIndentedString(load)).append("\n");
+ sb.append(" location: ").append(toIndentedString(location)).append("\n");
+ sb.append(" overloaded: ").append(toIndentedString(overloaded)).append("\n");
+ sb.append(" port: ").append(toIndentedString(port)).append("\n");
+ sb.append(" transport: ").append(toIndentedString(transport)).append("\n");
+ sb.append(" type: ").append(toIndentedString(type)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(java.lang.Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+
+}
+
diff --git a/app/src/main/java/io/swagger/client/model/ModelsLocation.java b/app/src/main/java/io/swagger/client/model/ModelsLocation.java
new file mode 100644
index 00000000..adda676b
--- /dev/null
+++ b/app/src/main/java/io/swagger/client/model/ModelsLocation.java
@@ -0,0 +1,301 @@
+/*
+ * Menshen API
+ * This is a LEAP VPN Service API
+ *
+ * OpenAPI spec version: 0.5.2
+ *
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+
+
+package io.swagger.client.model;
+
+import java.util.Objects;
+import java.util.Arrays;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import java.io.IOException;
+
+/**
+ * ModelsLocation
+ */
+
+public class ModelsLocation {
+ @SerializedName("country_code")
+ private String countryCode = null;
+
+ @SerializedName("display_name")
+ private String displayName = null;
+
+ @SerializedName("has_bridges")
+ private Boolean hasBridges = null;
+
+ @SerializedName("healthy")
+ private Boolean healthy = null;
+
+ @SerializedName("hemisphere")
+ private String hemisphere = null;
+
+ @SerializedName("label")
+ private String label = null;
+
+ @SerializedName("lat")
+ private String lat = null;
+
+ @SerializedName("lon")
+ private String lon = null;
+
+ @SerializedName("region")
+ private String region = null;
+
+ @SerializedName("timezone")
+ private String timezone = null;
+
+ public ModelsLocation countryCode(String countryCode) {
+ this.countryCode = countryCode;
+ return this;
+ }
+
+ /**
+ * CountryCode is the two-character country ISO identifier (uppercase).
+ * @return countryCode
+ **/
+ @ApiModelProperty(value = "CountryCode is the two-character country ISO identifier (uppercase).")
+ public String getCountryCode() {
+ return countryCode;
+ }
+
+ public void setCountryCode(String countryCode) {
+ this.countryCode = countryCode;
+ }
+
+ public ModelsLocation displayName(String displayName) {
+ this.displayName = displayName;
+ return this;
+ }
+
+ /**
+ * DisplayName is the user-facing string for a given location.
+ * @return displayName
+ **/
+ @ApiModelProperty(value = "DisplayName is the user-facing string for a given location.")
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
+ public ModelsLocation hasBridges(Boolean hasBridges) {
+ this.hasBridges = hasBridges;
+ return this;
+ }
+
+ /**
+ * Any location that has at least one bridge configured will set this to true.
+ * @return hasBridges
+ **/
+ @ApiModelProperty(value = "Any location that has at least one bridge configured will set this to true.")
+ public Boolean isHasBridges() {
+ return hasBridges;
+ }
+
+ public void setHasBridges(Boolean hasBridges) {
+ this.hasBridges = hasBridges;
+ }
+
+ public ModelsLocation healthy(Boolean healthy) {
+ this.healthy = healthy;
+ return this;
+ }
+
+ /**
+ * TODO Not used right now, but intended to signal when a location has all of their nodes overwhelmed.
+ * @return healthy
+ **/
+ @ApiModelProperty(value = "TODO Not used right now, but intended to signal when a location has all of their nodes overwhelmed.")
+ public Boolean isHealthy() {
+ return healthy;
+ }
+
+ public void setHealthy(Boolean healthy) {
+ this.healthy = healthy;
+ }
+
+ public ModelsLocation hemisphere(String hemisphere) {
+ this.hemisphere = hemisphere;
+ return this;
+ }
+
+ /**
+ * Hemisphere is a legacy label for a gateway. The rationale was once intended to be to allocate gateways for an hemisphere with certain regional \&quot;fairness\&quot;, even if they&#39;re geographically located in a different region. We might want to set this on the Gateway or Bridge, not in the Location itself...
+ * @return hemisphere
+ **/
+ @ApiModelProperty(value = "Hemisphere is a legacy label for a gateway. The rationale was once intended to be to allocate gateways for an hemisphere with certain regional \"fairness\", even if they're geographically located in a different region. We might want to set this on the Gateway or Bridge, not in the Location itself...")
+ public String getHemisphere() {
+ return hemisphere;
+ }
+
+ public void setHemisphere(String hemisphere) {
+ this.hemisphere = hemisphere;
+ }
+
+ public ModelsLocation label(String label) {
+ this.label = label;
+ return this;
+ }
+
+ /**
+ * Label is the short representation of a location, used internally.
+ * @return label
+ **/
+ @ApiModelProperty(value = "Label is the short representation of a location, used internally.")
+ public String getLabel() {
+ return label;
+ }
+
+ public void setLabel(String label) {
+ this.label = label;
+ }
+
+ public ModelsLocation lat(String lat) {
+ this.lat = lat;
+ return this;
+ }
+
+ /**
+ * Lat is the latitude for the location.
+ * @return lat
+ **/
+ @ApiModelProperty(value = "Lat is the latitude for the location.")
+ public String getLat() {
+ return lat;
+ }
+
+ public void setLat(String lat) {
+ this.lat = lat;
+ }
+
+ public ModelsLocation lon(String lon) {
+ this.lon = lon;
+ return this;
+ }
+
+ /**
+ * Lon is the longitude for the location.
+ * @return lon
+ **/
+ @ApiModelProperty(value = "Lon is the longitude for the location.")
+ public String getLon() {
+ return lon;
+ }
+
+ public void setLon(String lon) {
+ this.lon = lon;
+ }
+
+ public ModelsLocation region(String region) {
+ this.region = region;
+ return this;
+ }
+
+ /**
+ * Region is the continental region this gateway is assigned to. Not used at the moment, intended to use a label from the 7-continent model.
+ * @return region
+ **/
+ @ApiModelProperty(value = "Region is the continental region this gateway is assigned to. Not used at the moment, intended to use a label from the 7-continent model.")
+ public String getRegion() {
+ return region;
+ }
+
+ public void setRegion(String region) {
+ this.region = region;
+ }
+
+ public ModelsLocation timezone(String timezone) {
+ this.timezone = timezone;
+ return this;
+ }
+
+ /**
+ * Timezone is the TZ for the location (-1, 0, +1, ...)
+ * @return timezone
+ **/
+ @ApiModelProperty(value = "Timezone is the TZ for the location (-1, 0, +1, ...)")
+ public String getTimezone() {
+ return timezone;
+ }
+
+ public void setTimezone(String timezone) {
+ this.timezone = timezone;
+ }
+
+
+ @Override
+ public boolean equals(java.lang.Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ModelsLocation modelsLocation = (ModelsLocation) o;
+ return Objects.equals(this.countryCode, modelsLocation.countryCode) &&
+ Objects.equals(this.displayName, modelsLocation.displayName) &&
+ Objects.equals(this.hasBridges, modelsLocation.hasBridges) &&
+ Objects.equals(this.healthy, modelsLocation.healthy) &&
+ Objects.equals(this.hemisphere, modelsLocation.hemisphere) &&
+ Objects.equals(this.label, modelsLocation.label) &&
+ Objects.equals(this.lat, modelsLocation.lat) &&
+ Objects.equals(this.lon, modelsLocation.lon) &&
+ Objects.equals(this.region, modelsLocation.region) &&
+ Objects.equals(this.timezone, modelsLocation.timezone);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(countryCode, displayName, hasBridges, healthy, hemisphere, label, lat, lon, region, timezone);
+ }
+
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class ModelsLocation {\n");
+
+ sb.append(" countryCode: ").append(toIndentedString(countryCode)).append("\n");
+ sb.append(" displayName: ").append(toIndentedString(displayName)).append("\n");
+ sb.append(" hasBridges: ").append(toIndentedString(hasBridges)).append("\n");
+ sb.append(" healthy: ").append(toIndentedString(healthy)).append("\n");
+ sb.append(" hemisphere: ").append(toIndentedString(hemisphere)).append("\n");
+ sb.append(" label: ").append(toIndentedString(label)).append("\n");
+ sb.append(" lat: ").append(toIndentedString(lat)).append("\n");
+ sb.append(" lon: ").append(toIndentedString(lon)).append("\n");
+ sb.append(" region: ").append(toIndentedString(region)).append("\n");
+ sb.append(" timezone: ").append(toIndentedString(timezone)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(java.lang.Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+
+}
+
diff --git a/app/src/main/java/io/swagger/client/model/ModelsProvider.java b/app/src/main/java/io/swagger/client/model/ModelsProvider.java
new file mode 100644
index 00000000..34b54510
--- /dev/null
+++ b/app/src/main/java/io/swagger/client/model/ModelsProvider.java
@@ -0,0 +1,584 @@
+/*
+ * Menshen API
+ * This is a LEAP VPN Service API
+ *
+ * OpenAPI spec version: 0.5.2
+ *
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+
+
+package io.swagger.client.model;
+
+import java.util.Objects;
+import java.util.Arrays;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import io.swagger.client.model.ModelsProviderService;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * ModelsProvider
+ */
+
+public class ModelsProvider {
+ @SerializedName("api_uri")
+ private String apiUri = null;
+
+ @SerializedName("api_version")
+ private String apiVersion = null;
+
+ @SerializedName("api_versions")
+ private List<String> apiVersions = null;
+
+ @SerializedName("ask_for_donations")
+ private Boolean askForDonations = null;
+
+ @SerializedName("ca_cert_fingerprint")
+ private String caCertFingerprint = null;
+
+ @SerializedName("ca_cert_uri")
+ private String caCertUri = null;
+
+ @SerializedName("country_code_lookup_url")
+ private String countryCodeLookupUrl = null;
+
+ @SerializedName("default_language")
+ private String defaultLanguage = null;
+
+ @SerializedName("description")
+ private Map<String, String> description = null;
+
+ @SerializedName("domain")
+ private String domain = null;
+
+ @SerializedName("donate_period")
+ private String donatePeriod = null;
+
+ @SerializedName("donate_url")
+ private String donateUrl = null;
+
+ @SerializedName("info_url")
+ private String infoUrl = null;
+
+ @SerializedName("languages")
+ private List<String> languages = null;
+
+ @SerializedName("motd_url")
+ private String motdUrl = null;
+
+ @SerializedName("name")
+ private Map<String, String> name = null;
+
+ @SerializedName("service")
+ private ModelsProviderService service = null;
+
+ @SerializedName("services")
+ private List<String> services = null;
+
+ @SerializedName("stun_servers")
+ private List<String> stunServers = null;
+
+ @SerializedName("tos_url")
+ private String tosUrl = null;
+
+ public ModelsProvider apiUri(String apiUri) {
+ this.apiUri = apiUri;
+ return this;
+ }
+
+ /**
+ * URL of the API endpoints
+ * @return apiUri
+ **/
+ @ApiModelProperty(value = "URL of the API endpoints")
+ public String getApiUri() {
+ return apiUri;
+ }
+
+ public void setApiUri(String apiUri) {
+ this.apiUri = apiUri;
+ }
+
+ public ModelsProvider apiVersion(String apiVersion) {
+ this.apiVersion = apiVersion;
+ return this;
+ }
+
+ /**
+ * oldest supported api version deprecated: kept for backwards compatibility. Replaced by api_versions.
+ * @return apiVersion
+ **/
+ @ApiModelProperty(value = "oldest supported api version deprecated: kept for backwards compatibility. Replaced by api_versions.")
+ public String getApiVersion() {
+ return apiVersion;
+ }
+
+ public void setApiVersion(String apiVersion) {
+ this.apiVersion = apiVersion;
+ }
+
+ public ModelsProvider apiVersions(List<String> apiVersions) {
+ this.apiVersions = apiVersions;
+ return this;
+ }
+
+ public ModelsProvider addApiVersionsItem(String apiVersionsItem) {
+ if (this.apiVersions == null) {
+ this.apiVersions = new ArrayList<String>();
+ }
+ this.apiVersions.add(apiVersionsItem);
+ return this;
+ }
+
+ /**
+ * all API versions the provider supports
+ * @return apiVersions
+ **/
+ @ApiModelProperty(value = "all API versions the provider supports")
+ public List<String> getApiVersions() {
+ return apiVersions;
+ }
+
+ public void setApiVersions(List<String> apiVersions) {
+ this.apiVersions = apiVersions;
+ }
+
+ public ModelsProvider askForDonations(Boolean askForDonations) {
+ this.askForDonations = askForDonations;
+ return this;
+ }
+
+ /**
+ * Flag indicating whether to show regularly a donation reminder
+ * @return askForDonations
+ **/
+ @ApiModelProperty(value = "Flag indicating whether to show regularly a donation reminder")
+ public Boolean isAskForDonations() {
+ return askForDonations;
+ }
+
+ public void setAskForDonations(Boolean askForDonations) {
+ this.askForDonations = askForDonations;
+ }
+
+ public ModelsProvider caCertFingerprint(String caCertFingerprint) {
+ this.caCertFingerprint = caCertFingerprint;
+ return this;
+ }
+
+ /**
+ * fingerprint of CA cert used to setup TLS sessions during VPN setup (and up to API version 3 for API communication) deprecated: kept for backwards compatibility
+ * @return caCertFingerprint
+ **/
+ @ApiModelProperty(value = "fingerprint of CA cert used to setup TLS sessions during VPN setup (and up to API version 3 for API communication) deprecated: kept for backwards compatibility")
+ public String getCaCertFingerprint() {
+ return caCertFingerprint;
+ }
+
+ public void setCaCertFingerprint(String caCertFingerprint) {
+ this.caCertFingerprint = caCertFingerprint;
+ }
+
+ public ModelsProvider caCertUri(String caCertUri) {
+ this.caCertUri = caCertUri;
+ return this;
+ }
+
+ /**
+ * URL to fetch the CA cert used to setup TLS sessions during VPN setup (and up to API version 3 for API communication) deprecated: kept for backwards compatibility
+ * @return caCertUri
+ **/
+ @ApiModelProperty(value = "URL to fetch the CA cert used to setup TLS sessions during VPN setup (and up to API version 3 for API communication) deprecated: kept for backwards compatibility")
+ public String getCaCertUri() {
+ return caCertUri;
+ }
+
+ public void setCaCertUri(String caCertUri) {
+ this.caCertUri = caCertUri;
+ }
+
+ public ModelsProvider countryCodeLookupUrl(String countryCodeLookupUrl) {
+ this.countryCodeLookupUrl = countryCodeLookupUrl;
+ return this;
+ }
+
+ /**
+ * URL of a service that returns a country code for an ip address. If empty, OONI backend is used
+ * @return countryCodeLookupUrl
+ **/
+ @ApiModelProperty(value = "URL of a service that returns a country code for an ip address. If empty, OONI backend is used")
+ public String getCountryCodeLookupUrl() {
+ return countryCodeLookupUrl;
+ }
+
+ public void setCountryCodeLookupUrl(String countryCodeLookupUrl) {
+ this.countryCodeLookupUrl = countryCodeLookupUrl;
+ }
+
+ public ModelsProvider defaultLanguage(String defaultLanguage) {
+ this.defaultLanguage = defaultLanguage;
+ return this;
+ }
+
+ /**
+ * Default language this provider uses to show infos and provider messages
+ * @return defaultLanguage
+ **/
+ @ApiModelProperty(value = "Default language this provider uses to show infos and provider messages")
+ public String getDefaultLanguage() {
+ return defaultLanguage;
+ }
+
+ public void setDefaultLanguage(String defaultLanguage) {
+ this.defaultLanguage = defaultLanguage;
+ }
+
+ public ModelsProvider description(Map<String, String> description) {
+ this.description = description;
+ return this;
+ }
+
+ public ModelsProvider putDescriptionItem(String key, String descriptionItem) {
+ if (this.description == null) {
+ this.description = new HashMap<String, String>();
+ }
+ this.description.put(key, descriptionItem);
+ return this;
+ }
+
+ /**
+ * Short description about the provider
+ * @return description
+ **/
+ @ApiModelProperty(value = "Short description about the provider")
+ public Map<String, String> getDescription() {
+ return description;
+ }
+
+ public void setDescription(Map<String, String> description) {
+ this.description = description;
+ }
+
+ public ModelsProvider domain(String domain) {
+ this.domain = domain;
+ return this;
+ }
+
+ /**
+ * Domain of the provider
+ * @return domain
+ **/
+ @ApiModelProperty(value = "Domain of the provider")
+ public String getDomain() {
+ return domain;
+ }
+
+ public void setDomain(String domain) {
+ this.domain = domain;
+ }
+
+ public ModelsProvider donatePeriod(String donatePeriod) {
+ this.donatePeriod = donatePeriod;
+ return this;
+ }
+
+ /**
+ * Number of days until a donation reminder reappears
+ * @return donatePeriod
+ **/
+ @ApiModelProperty(value = "Number of days until a donation reminder reappears")
+ public String getDonatePeriod() {
+ return donatePeriod;
+ }
+
+ public void setDonatePeriod(String donatePeriod) {
+ this.donatePeriod = donatePeriod;
+ }
+
+ public ModelsProvider donateUrl(String donateUrl) {
+ this.donateUrl = donateUrl;
+ return this;
+ }
+
+ /**
+ * URL to the donation website
+ * @return donateUrl
+ **/
+ @ApiModelProperty(value = "URL to the donation website")
+ public String getDonateUrl() {
+ return donateUrl;
+ }
+
+ public void setDonateUrl(String donateUrl) {
+ this.donateUrl = donateUrl;
+ }
+
+ public ModelsProvider infoUrl(String infoUrl) {
+ this.infoUrl = infoUrl;
+ return this;
+ }
+
+ /**
+ * URL to general provider website
+ * @return infoUrl
+ **/
+ @ApiModelProperty(value = "URL to general provider website")
+ public String getInfoUrl() {
+ return infoUrl;
+ }
+
+ public void setInfoUrl(String infoUrl) {
+ this.infoUrl = infoUrl;
+ }
+
+ public ModelsProvider languages(List<String> languages) {
+ this.languages = languages;
+ return this;
+ }
+
+ public ModelsProvider addLanguagesItem(String languagesItem) {
+ if (this.languages == null) {
+ this.languages = new ArrayList<String>();
+ }
+ this.languages.add(languagesItem);
+ return this;
+ }
+
+ /**
+ * Languages the provider supports to show infos and provider messages
+ * @return languages
+ **/
+ @ApiModelProperty(value = "Languages the provider supports to show infos and provider messages")
+ public List<String> getLanguages() {
+ return languages;
+ }
+
+ public void setLanguages(List<String> languages) {
+ this.languages = languages;
+ }
+
+ public ModelsProvider motdUrl(String motdUrl) {
+ this.motdUrl = motdUrl;
+ return this;
+ }
+
+ /**
+ * URL to the message of the day service
+ * @return motdUrl
+ **/
+ @ApiModelProperty(value = "URL to the message of the day service")
+ public String getMotdUrl() {
+ return motdUrl;
+ }
+
+ public void setMotdUrl(String motdUrl) {
+ this.motdUrl = motdUrl;
+ }
+
+ public ModelsProvider name(Map<String, String> name) {
+ this.name = name;
+ return this;
+ }
+
+ public ModelsProvider putNameItem(String key, String nameItem) {
+ if (this.name == null) {
+ this.name = new HashMap<String, String>();
+ }
+ this.name.put(key, nameItem);
+ return this;
+ }
+
+ /**
+ * Provider name
+ * @return name
+ **/
+ @ApiModelProperty(value = "Provider name")
+ public Map<String, String> getName() {
+ return name;
+ }
+
+ public void setName(Map<String, String> name) {
+ this.name = name;
+ }
+
+ public ModelsProvider service(ModelsProviderService service) {
+ this.service = service;
+ return this;
+ }
+
+ /**
+ * Get service
+ * @return service
+ **/
+ @ApiModelProperty(value = "")
+ public ModelsProviderService getService() {
+ return service;
+ }
+
+ public void setService(ModelsProviderService service) {
+ this.service = service;
+ }
+
+ public ModelsProvider services(List<String> services) {
+ this.services = services;
+ return this;
+ }
+
+ public ModelsProvider addServicesItem(String servicesItem) {
+ if (this.services == null) {
+ this.services = new ArrayList<String>();
+ }
+ this.services.add(servicesItem);
+ return this;
+ }
+
+ /**
+ * List of services the provider offers, currently only openvpn
+ * @return services
+ **/
+ @ApiModelProperty(value = "List of services the provider offers, currently only openvpn")
+ public List<String> getServices() {
+ return services;
+ }
+
+ public void setServices(List<String> services) {
+ this.services = services;
+ }
+
+ public ModelsProvider stunServers(List<String> stunServers) {
+ this.stunServers = stunServers;
+ return this;
+ }
+
+ public ModelsProvider addStunServersItem(String stunServersItem) {
+ if (this.stunServers == null) {
+ this.stunServers = new ArrayList<String>();
+ }
+ this.stunServers.add(stunServersItem);
+ return this;
+ }
+
+ /**
+ * list of STUN servers (format: ip/hostname:port) servers to get current ip address can consist of self hosted STUN servers, public ones or a combination of both. GeolocationLookup is only done when the list of STUNServers is not empty
+ * @return stunServers
+ **/
+ @ApiModelProperty(value = "list of STUN servers (format: ip/hostname:port) servers to get current ip address can consist of self hosted STUN servers, public ones or a combination of both. GeolocationLookup is only done when the list of STUNServers is not empty")
+ public List<String> getStunServers() {
+ return stunServers;
+ }
+
+ public void setStunServers(List<String> stunServers) {
+ this.stunServers = stunServers;
+ }
+
+ public ModelsProvider tosUrl(String tosUrl) {
+ this.tosUrl = tosUrl;
+ return this;
+ }
+
+ /**
+ * URL to Terms of Service website
+ * @return tosUrl
+ **/
+ @ApiModelProperty(value = "URL to Terms of Service website")
+ public String getTosUrl() {
+ return tosUrl;
+ }
+
+ public void setTosUrl(String tosUrl) {
+ this.tosUrl = tosUrl;
+ }
+
+
+ @Override
+ public boolean equals(java.lang.Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ModelsProvider modelsProvider = (ModelsProvider) o;
+ return Objects.equals(this.apiUri, modelsProvider.apiUri) &&
+ Objects.equals(this.apiVersion, modelsProvider.apiVersion) &&
+ Objects.equals(this.apiVersions, modelsProvider.apiVersions) &&
+ Objects.equals(this.askForDonations, modelsProvider.askForDonations) &&
+ Objects.equals(this.caCertFingerprint, modelsProvider.caCertFingerprint) &&
+ Objects.equals(this.caCertUri, modelsProvider.caCertUri) &&
+ Objects.equals(this.countryCodeLookupUrl, modelsProvider.countryCodeLookupUrl) &&
+ Objects.equals(this.defaultLanguage, modelsProvider.defaultLanguage) &&
+ Objects.equals(this.description, modelsProvider.description) &&
+ Objects.equals(this.domain, modelsProvider.domain) &&
+ Objects.equals(this.donatePeriod, modelsProvider.donatePeriod) &&
+ Objects.equals(this.donateUrl, modelsProvider.donateUrl) &&
+ Objects.equals(this.infoUrl, modelsProvider.infoUrl) &&
+ Objects.equals(this.languages, modelsProvider.languages) &&
+ Objects.equals(this.motdUrl, modelsProvider.motdUrl) &&
+ Objects.equals(this.name, modelsProvider.name) &&
+ Objects.equals(this.service, modelsProvider.service) &&
+ Objects.equals(this.services, modelsProvider.services) &&
+ Objects.equals(this.stunServers, modelsProvider.stunServers) &&
+ Objects.equals(this.tosUrl, modelsProvider.tosUrl);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(apiUri, apiVersion, apiVersions, askForDonations, caCertFingerprint, caCertUri, countryCodeLookupUrl, defaultLanguage, description, domain, donatePeriod, donateUrl, infoUrl, languages, motdUrl, name, service, services, stunServers, tosUrl);
+ }
+
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class ModelsProvider {\n");
+
+ sb.append(" apiUri: ").append(toIndentedString(apiUri)).append("\n");
+ sb.append(" apiVersion: ").append(toIndentedString(apiVersion)).append("\n");
+ sb.append(" apiVersions: ").append(toIndentedString(apiVersions)).append("\n");
+ sb.append(" askForDonations: ").append(toIndentedString(askForDonations)).append("\n");
+ sb.append(" caCertFingerprint: ").append(toIndentedString(caCertFingerprint)).append("\n");
+ sb.append(" caCertUri: ").append(toIndentedString(caCertUri)).append("\n");
+ sb.append(" countryCodeLookupUrl: ").append(toIndentedString(countryCodeLookupUrl)).append("\n");
+ sb.append(" defaultLanguage: ").append(toIndentedString(defaultLanguage)).append("\n");
+ sb.append(" description: ").append(toIndentedString(description)).append("\n");
+ sb.append(" domain: ").append(toIndentedString(domain)).append("\n");
+ sb.append(" donatePeriod: ").append(toIndentedString(donatePeriod)).append("\n");
+ sb.append(" donateUrl: ").append(toIndentedString(donateUrl)).append("\n");
+ sb.append(" infoUrl: ").append(toIndentedString(infoUrl)).append("\n");
+ sb.append(" languages: ").append(toIndentedString(languages)).append("\n");
+ sb.append(" motdUrl: ").append(toIndentedString(motdUrl)).append("\n");
+ sb.append(" name: ").append(toIndentedString(name)).append("\n");
+ sb.append(" service: ").append(toIndentedString(service)).append("\n");
+ sb.append(" services: ").append(toIndentedString(services)).append("\n");
+ sb.append(" stunServers: ").append(toIndentedString(stunServers)).append("\n");
+ sb.append(" tosUrl: ").append(toIndentedString(tosUrl)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(java.lang.Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+
+}
+
diff --git a/app/src/main/java/io/swagger/client/model/ModelsProviderService.java b/app/src/main/java/io/swagger/client/model/ModelsProviderService.java
new file mode 100644
index 00000000..ef13b60d
--- /dev/null
+++ b/app/src/main/java/io/swagger/client/model/ModelsProviderService.java
@@ -0,0 +1,118 @@
+/*
+ * Menshen API
+ * This is a LEAP VPN Service API
+ *
+ * OpenAPI spec version: 0.5.2
+ *
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+
+
+package io.swagger.client.model;
+
+import java.util.Objects;
+import java.util.Arrays;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.JsonAdapter;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import java.io.IOException;
+
+/**
+ * Operational properties which describe how the provider offers the service
+ */
+@ApiModel(description = "Operational properties which describe how the provider offers the service")
+
+public class ModelsProviderService {
+ @SerializedName("allow_anonymous")
+ private Boolean allowAnonymous = null;
+
+ @SerializedName("allow_registration")
+ private Boolean allowRegistration = null;
+
+ public ModelsProviderService allowAnonymous(Boolean allowAnonymous) {
+ this.allowAnonymous = allowAnonymous;
+ return this;
+ }
+
+ /**
+ * Flag indicating if anonymous usage without registration is allowed deprecated: kept for backwards compatibility
+ * @return allowAnonymous
+ **/
+ @ApiModelProperty(value = "Flag indicating if anonymous usage without registration is allowed deprecated: kept for backwards compatibility")
+ public Boolean isAllowAnonymous() {
+ return allowAnonymous;
+ }
+
+ public void setAllowAnonymous(Boolean allowAnonymous) {
+ this.allowAnonymous = allowAnonymous;
+ }
+
+ public ModelsProviderService allowRegistration(Boolean allowRegistration) {
+ this.allowRegistration = allowRegistration;
+ return this;
+ }
+
+ /**
+ * Flag indicating if the provider supports user registration deprecated: kept for backwards compatibility
+ * @return allowRegistration
+ **/
+ @ApiModelProperty(value = "Flag indicating if the provider supports user registration deprecated: kept for backwards compatibility")
+ public Boolean isAllowRegistration() {
+ return allowRegistration;
+ }
+
+ public void setAllowRegistration(Boolean allowRegistration) {
+ this.allowRegistration = allowRegistration;
+ }
+
+
+ @Override
+ public boolean equals(java.lang.Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ModelsProviderService modelsProviderService = (ModelsProviderService) o;
+ return Objects.equals(this.allowAnonymous, modelsProviderService.allowAnonymous) &&
+ Objects.equals(this.allowRegistration, modelsProviderService.allowRegistration);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(allowAnonymous, allowRegistration);
+ }
+
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class ModelsProviderService {\n");
+
+ sb.append(" allowAnonymous: ").append(toIndentedString(allowAnonymous)).append("\n");
+ sb.append(" allowRegistration: ").append(toIndentedString(allowRegistration)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(java.lang.Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+
+}
+
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java b/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java
index c7e12491..6b3ba348 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java
@@ -35,8 +35,10 @@ import androidx.lifecycle.ProcessLifecycleOwner;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.multidex.MultiDexApplication;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.conscrypt.Conscrypt;
+import java.security.Provider;
import java.security.Security;
import se.leap.bitmaskclient.BuildConfig;
@@ -70,7 +72,14 @@ public class BitmaskApp extends MultiDexApplication implements DefaultLifecycleO
super.onCreate();
// Normal app init code...*/
PRNGFixes.apply();
- Security.insertProviderAt(Conscrypt.newProvider(), 1);
+ final Provider provider = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME);
+ // Replace Android's own BC provider
+ if (!provider.getClass().equals(BouncyCastleProvider.class)) {
+ Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
+ Security.insertProviderAt(new BouncyCastleProvider(), 1);
+ }
+ Security.insertProviderAt(Conscrypt.newProvider(), 2);
+
preferenceHelper = new PreferenceHelper(this);
providerObservable = ProviderObservable.getInstance();
providerObservable.updateProvider(getSavedProviderFromSharedPreferences());
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/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 33c8f388..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,6 +3,7 @@ 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;
@@ -17,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;
@@ -35,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;
@@ -78,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);
@@ -89,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) {
@@ -224,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;
}
@@ -260,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")) {
+ if (!BuildConfig.DEBUG_MODE) {
obfuscationPinning.setVisibility(GONE);
return;
}
@@ -350,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/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 64e57cda..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,7 +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.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;
@@ -37,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>
@@ -68,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;
@@ -96,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",
@@ -116,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);
@@ -178,61 +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() {
- 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), 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;
}
@@ -252,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 = "";
}
}
@@ -277,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;
}
@@ -334,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() {
@@ -359,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("."));
}
}
@@ -390,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
@@ -406,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);
@@ -424,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) : "");
+ }
}
@@ -431,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;
@@ -442,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()) {
@@ -471,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()) {
@@ -485,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();
}
@@ -496,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;
}
@@ -531,7 +721,7 @@ public final class Provider implements Parcelable {
@Override
public int hashCode() {
- return getMainUrlString().hashCode();
+ return getMainUrl().hashCode();
}
@Override
@@ -541,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;
}
@@ -596,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;
}
/**
@@ -632,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) {
@@ -689,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() {
@@ -740,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)
@@ -749,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 c2590012..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,11 +23,17 @@ 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 final String type;
@@ -32,6 +54,13 @@ public class Transport implements Serializable {
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 final String cert;
@SerializedName("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);
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 22af1bfb..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
@@ -15,7 +15,7 @@ public class BuildConfigHelper {
String obfsvpnIP();
String obfsvpnPort();
String obfsvpnCert();
- boolean useKcp();
+ String obfsvpnTransportProtocol();
boolean isDefaultBitmask();
}
@@ -26,9 +26,11 @@ public class BuildConfigHelper {
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
@@ -47,8 +49,8 @@ public class BuildConfigHelper {
}
@Override
- public boolean useKcp() {
- return BuildConfig.obfsvpn_use_kcp;
+ public String obfsvpnTransportProtocol() {
+ return BuildConfig.obfsvpn_transport_protocol;
}
@Override
@@ -79,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/PreferenceHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
index 8d1f21e5..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);
}
@@ -552,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);
}
@@ -576,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);
}
@@ -630,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);
@@ -737,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 bd626ce5..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;
@@ -200,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()) {
@@ -208,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);
@@ -218,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);
@@ -386,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);
}
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 8cbc4289..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,37 +19,40 @@ 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.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 de.blinkt.openvpn.core.connection.Obfs4Connection;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.base.models.Transport;
import se.leap.bitmaskclient.base.utils.ConfigHelper;
@@ -58,9 +61,8 @@ 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;
@@ -68,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;
@@ -82,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;
@@ -103,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
@@ -174,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);
@@ -191,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);
}
@@ -209,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(" "))
@@ -229,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();
}
@@ -267,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 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 @Nullable Transport getTransport(TransportType transportType) {
+ for (Transport transport : transports) {
+ if (transport.getTransportType() == transportType) {
+ return transport;
+ }
+ }
+ return null;
+ }
+
+ 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;
}
@@ -330,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;
}
@@ -382,7 +379,7 @@ public class VpnConfigGenerator {
}
stringBuilder.append(getRouteString(ipAddress, transport));
- String transparentProxyRemote = REMOTE + " " + ObfsvpnClient.IP + " " + ObfsvpnClient.PORT + " udp" + newLine;
+ String transparentProxyRemote = REMOTE + " " + ObfsvpnClient.IP + " " + ObfsvpnClient.DEFAULT_PORT + " udp" + newLine;
stringBuilder.append(transparentProxyRemote);
}
@@ -426,7 +423,7 @@ public class VpnConfigGenerator {
// 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
@@ -455,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/ObfsvpnClient.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsvpnClient.java
index bf030a0f..2e216015 100644
--- a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsvpnClient.java
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/ObfsvpnClient.java
@@ -1,89 +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.base.models.Constants;
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 PORT = 8080;
+ 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 {
-
- //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 kcpEnabled = Constants.KCP.equals(options.transport.getProtocols()[0]);
+ // 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);
- HoppingConfig hoppingConfig = new HoppingConfig(hoppingEnabled,IP+":"+PORT, options, 10, 10);
- ObfsvpnConfig obfsvpnConfig = new ObfsvpnConfig(IP+":"+PORT, hoppingConfig, kcpConfig, options.bridgeIP, options.transport.getPorts()[0], options.transport.getOptions().getCert() );
+ 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, obfsvpnConfig.toString());
+ Log.d(TAG, "create new obfsvpn client: " + obfsvpnConfig);
client = Client.newFFIClient(obfsvpnConfig.toString());
- client.setEventLogger(this);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
- public int start() {
-
+ 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 {
- client.start();
- } catch (Exception e) {
+ start(0);
+ } catch (RuntimeException e) {
+ // save exception and stop blocking
e.printStackTrace();
+ err.set(e);
+ callback.countDown();
}
}).start();
- return PORT;
+
+ 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);
+ }
}
}
- public void stop() {
+ 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("[obfs4-client] " + s);
-
+ VpnStatus.logError("[obfsvpn-client] error: " + s);
}
@Override
public void log(String state, String message) {
- VpnStatus.logDebug("[obfs4-client] " + state + ": " + 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/models/HoppingConfig.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/HoppingConfig.java
index 96b8c460..0dc2d508 100644
--- a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/HoppingConfig.java
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/HoppingConfig.java
@@ -11,14 +11,15 @@ import se.leap.bitmaskclient.base.models.Transport;
public class HoppingConfig {
/**
- * Enabled bool `json:"enabled"`
+ * 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;
@@ -29,12 +30,12 @@ public class HoppingConfig {
final int portCount;
final int minHopSeconds;
final int hopJitter;
+ final int minHopPort;
+ final int maxHopPort;
public HoppingConfig(boolean enabled,
String proxyAddr,
- Obfs4Options options,
- int minHopSeconds,
- int hopJitter) {
+ Obfs4Options options) {
this.enabled = enabled;
this.proxyAddr = proxyAddr;
Transport transport = options.transport;
@@ -54,8 +55,10 @@ public class HoppingConfig {
}
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
index 255e7dd7..f056394d 100644
--- a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/KcpConfig.java
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/KcpConfig.java
@@ -5,27 +5,25 @@ 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 {
- // check OpenVPN's --sndbuf size and --rcvbuf size
- public static final int DEFAULT_KCP_SEND_WINDOW_SIZE = 32;
- public static final int DEFAULT_KCP_RECEIVE_WINDOW_SIZE = 32;
- public static final int DEFAULT_KCP_READ_BUFFER = 16 * 1024 * 1024;
- public static final int DEFAULT_KCP_WRITE_BUFFER = 16 * 1024 * 1024;
final boolean enabled;
- final int sendWindowSize;
- final int receiveWindowSize;
- final int readBuffer;
- final int writeBuffer;
+ 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;
- this.sendWindowSize = DEFAULT_KCP_SEND_WINDOW_SIZE;
- this.receiveWindowSize = DEFAULT_KCP_RECEIVE_WINDOW_SIZE;
- this.readBuffer = DEFAULT_KCP_READ_BUFFER;
- this.writeBuffer = DEFAULT_KCP_WRITE_BUFFER;
}
@NonNull
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
index 9f85c4a0..cfcd6b6c 100644
--- a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/ObfsvpnConfig.java
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/models/ObfsvpnConfig.java
@@ -11,14 +11,16 @@ 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, String remoteIP, String remotePort, String obfsv4Cert) {
+ 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;
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 237f5bd2..d57e1739 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupObservable.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupObservable.java
@@ -38,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;
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 9a0bf7b7..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
@@ -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.SetupFragmentFactory.PROVIDER_SELECTION_FRAGMENT;
import static se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus.OFF;
import android.Manifest;
@@ -30,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;
@@ -39,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;
@@ -51,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;
@@ -74,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);
}
@@ -92,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) {
@@ -145,6 +160,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;
}
@@ -156,7 +174,7 @@ public class SetupActivity extends AppCompatActivity implements SetupActivityCal
setupActionBar();
if (ProviderSetupObservable.isSetupRunning()) {
- provider = ProviderSetupObservable.getResultData().getParcelable(PROVIDER_KEY);
+ provider = BundleCompat.getParcelable(ProviderSetupObservable.getResultData(), PROVIDER_KEY, Provider.class);
if (provider != null) {
currentPosition = adapter.getFragmentPostion(CONFIGURE_PROVIDER_FRAGMENT);
}
@@ -164,6 +182,51 @@ public class SetupActivity extends AppCompatActivity implements SetupActivityCal
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
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
@@ -333,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 621cb41a..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);
@@ -135,8 +144,18 @@ public class ConfigureProviderFragment extends BaseSetupFragment implements Prop
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();
- ProviderAPICommand.execute(getContext(), SET_UP_PROVIDER, setupActivityCallback.getSelectedProvider());
+ Bundle parameters = new Bundle();
+ parameters.putString(Constants.COUNTRYCODE, PreferenceHelper.getBaseCountry());
+ ProviderAPICommand.execute(getContext(), SET_UP_PROVIDER, parameters, setupActivityCallback.getSelectedProvider());
}
}
@@ -211,34 +230,31 @@ public class ConfigureProviderFragment extends BaseSetupFragment implements Prop
}
private void handleResult(int resultCode, Bundle resultData, boolean resumeSetup) {
- Provider provider = resultData.getParcelable(PROVIDER_KEY);
+ 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:
setupActivityCallback.onProviderSelected(provider);
- if (provider.allowsAnonymous()) {
- ProviderAPICommand.execute(this.getContext(), DOWNLOAD_VPN_CERTIFICATE, 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()) {
- try {
- setupActivityCallback.onConfigurationSuccess();
- } catch (NullPointerException npe) {
- // callback disappeared in the meanwhile
- }
- }
- }, resumeSetup ? 0 : 750);
+ sendSuccess(resumeSetup);
break;
case PROVIDER_NOK:
case INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
@@ -252,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/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() {
diff --git a/app/src/main/res/drawable/bridge_automatic.xml b/app/src/main/res/drawable/bridge_automatic.xml
new file mode 100644
index 00000000..28c288f0
--- /dev/null
+++ b/app/src/main/res/drawable/bridge_automatic.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="16dp"
+ android:height="16dp"
+ android:viewportWidth="16"
+ android:viewportHeight="16"
+ android:tint="#828282">
+ <path
+ android:pathData="M2,6.904V5.406C3.072,5.117 4.148,4.912 5.226,4.792C6.304,4.671 7.395,4.611 8.5,4.611C9.605,4.611 10.696,4.671 11.774,4.792C12.852,4.912 13.927,5.117 15,5.406V6.904C14.783,6.844 14.567,6.79 14.35,6.742L13.7,6.597V8.944H12.4V6.344C11.75,6.248 11.1,6.176 10.45,6.128C9.8,6.08 9.15,6.056 8.5,6.056C7.85,6.056 7.2,6.077 6.55,6.119C5.9,6.161 5.25,6.23 4.6,6.326V8.944H3.3V6.579C3.083,6.627 2.867,6.678 2.65,6.733C2.433,6.787 2.217,6.844 2,6.904ZM4.6,14L5.543,7.662C5.738,7.638 5.96,7.617 6.209,7.599C6.458,7.581 6.68,7.566 6.875,7.554L5.9,14H4.6ZM6.55,1H7.85L7.525,3.149C7.33,3.161 7.111,3.176 6.867,3.194C6.623,3.212 6.404,3.233 6.209,3.257L6.55,1ZM7.85,14H9.15V11.111H7.85V14ZM7.85,9.667H9.15V7.518H7.85V9.667ZM9.15,1H10.45L10.791,3.257C10.596,3.245 10.377,3.227 10.133,3.203C9.889,3.179 9.67,3.161 9.475,3.149L9.15,1ZM11.1,14L10.125,7.554C10.32,7.566 10.542,7.584 10.791,7.608C11.04,7.632 11.262,7.656 11.458,7.681L12.4,14H11.1Z"
+ android:fillColor="#1C1B1F"/>
+</vector>
diff --git a/app/src/main/res/drawable/bridge_manual.xml b/app/src/main/res/drawable/bridge_manual.xml
new file mode 100644
index 00000000..cec55dab
--- /dev/null
+++ b/app/src/main/res/drawable/bridge_manual.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="16dp"
+ android:height="16dp"
+ android:viewportWidth="16"
+ android:viewportHeight="16"
+ android:tint="#828282">
+ <path
+ android:pathData="M5.6,8.084V2H4.4V8.084C3.368,8.354 2.6,9.284 2.6,10.4C2.6,11.516 3.368,12.446 4.4,12.716V14H5.6V12.716C6.632,12.446 7.4,11.516 7.4,10.4C7.4,9.284 6.632,8.354 5.6,8.084ZM5,9.2C5.66,9.2 6.2,9.74 6.2,10.4C6.2,11.06 5.66,11.6 5,11.6C4.34,11.6 3.8,11.06 3.8,10.4C3.8,9.74 4.34,9.2 5,9.2ZM11.6,2H10.4V3.284C9.368,3.554 8.6,4.484 8.6,5.6C8.6,6.716 9.368,7.646 10.4,7.916V14H11.6V7.916C12.632,7.646 13.4,6.716 13.4,5.6C13.4,4.484 12.632,3.554 11.6,3.284V2ZM11,4.4C11.66,4.4 12.2,4.94 12.2,5.6C12.2,6.26 11.66,6.8 11,6.8C10.34,6.8 9.8,6.26 9.8,5.6C9.8,4.94 10.34,4.4 11,4.4Z"
+ android:fillColor="#000000"/>
+</vector>
diff --git a/app/src/main/res/drawable/qr_code_scanner.xml b/app/src/main/res/drawable/qr_code_scanner.xml
new file mode 100644
index 00000000..5ab50c71
--- /dev/null
+++ b/app/src/main/res/drawable/qr_code_scanner.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M9.5,6.5v3h-3v-3H9.5M11,5H5v6h6V5L11,5zM9.5,14.5v3h-3v-3H9.5M11,13H5v6h6V13L11,13zM17.5,6.5v3h-3v-3H17.5M19,5h-6v6h6V5L19,5zM13,13h1.5v1.5H13V13zM14.5,14.5H16V16h-1.5V14.5zM16,13h1.5v1.5H16V13zM13,16h1.5v1.5H13V16zM14.5,17.5H16V19h-1.5V17.5zM16,16h1.5v1.5H16V16zM17.5,14.5H19V16h-1.5V14.5zM17.5,17.5H19V19h-1.5V17.5zM22,7h-2V4h-3V2h5V7zM22,22v-5h-2v3h-3v2H22zM2,22h5v-2H4v-3H2V22zM2,2v5h2V4h3V2H2z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/translate.xml b/app/src/main/res/drawable/translate.xml
new file mode 100644
index 00000000..7826980c
--- /dev/null
+++ b/app/src/main/res/drawable/translate.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#828282" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M12.87,15.07l-2.54,-2.51 0.03,-0.03c1.74,-1.94 2.98,-4.17 3.71,-6.53L17,6L17,4h-7L10,2L8,2v2L1,4v1.99h11.17C11.5,7.92 10.44,9.75 9,11.35 8.07,10.32 7.3,9.19 6.69,8h-2c0.73,1.63 1.73,3.17 2.98,4.56l-5.09,5.02L4,19l5,-5 3.11,3.11 0.76,-2.04zM18.5,10h-2L12,22h2l1.12,-3h4.75L21,22h2l-4.5,-12zM15.88,17l1.62,-4.33L19.12,17h-3.24z"/>
+
+</vector>
diff --git a/app/src/main/res/layout/d_obfuscation_proxy.xml b/app/src/main/res/layout/d_obfuscation_proxy.xml
index 7b8fcaa7..03ffb61f 100644
--- a/app/src/main/res/layout/d_obfuscation_proxy.xml
+++ b/app/src/main/res/layout/d_obfuscation_proxy.xml
@@ -57,16 +57,32 @@
android:id="@+id/cert_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
- <se.leap.bitmaskclient.base.views.IconSwitchEntry
- android:id="@+id/kcp_switch"
+
+ <TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
- app:text="KCP"
- app:subtitle="UDP based network protocol"
- app:icon="@drawable/ic_multiple_stop"
- >
-
- </se.leap.bitmaskclient.base.views.IconSwitchEntry>
+ android:text="Network protocol"
+ android:textStyle="bold"
+ android:paddingTop="@dimen/activity_margin"
+ android:textAppearance="@android:style/TextAppearance.DeviceDefault" />
+ <androidx.appcompat.widget.LinearLayoutCompat
+ android:id="@+id/protocol_spinner_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+ <androidx.appcompat.widget.AppCompatImageView
+ android:layout_width="?android:attr/listPreferredItemHeightSmall"
+ android:layout_height="?android:attr/listPreferredItemHeightSmall"
+ android:src="@drawable/ic_multiple_stop"
+ android:padding="@dimen/stdpadding"/>
+ <androidx.appcompat.widget.AppCompatSpinner
+ android:id="@+id/protocol_spinner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:tooltipText="select nework protocol"/>
+ </androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
diff --git a/app/src/main/res/layout/f_censorship_circumvention.xml b/app/src/main/res/layout/f_censorship_circumvention.xml
new file mode 100644
index 00000000..69ee7afd
--- /dev/null
+++ b/app/src/main/res/layout/f_censorship_circumvention.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="@dimen/activity_margin"
+ tools:context=".base.fragments.CensorshipCircumventionFragment">
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/censorship_circumvention_description" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/discovery"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/activity_margin"
+ android:text="@string/discovery"
+ android:textAppearance="@style/TextAppearance.AppCompat.Title" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/stdpadding"
+ android:text="@string/discovery_description" />
+
+ <RadioGroup
+ android:id="@+id/discovery_radioGroup"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/stdpadding" />
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/tunnelling"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/activity_margin"
+ android:text="@string/tunnelling"
+ android:textAppearance="@style/TextAppearance.AppCompat.Title" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/stdpadding"
+ android:text="@string/tunnelling_description" />
+
+ <RadioGroup
+ android:id="@+id/tunneling_radioGroup"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/stdpadding" />
+
+ <se.leap.bitmaskclient.base.views.IconSwitchEntry
+ android:id="@+id/port_hopping_switch"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/stdpadding"
+ app:singleLine="false"
+ app:subtitle="@string/port_hopping_description"
+ app:text="@string/port_hopping" />
+
+ </LinearLayout>
+</ScrollView> \ No newline at end of file
diff --git a/app/src/main/res/layout/f_drawer_main.xml b/app/src/main/res/layout/f_drawer_main.xml
index a948d9ce..ca0406ad 100644
--- a/app/src/main/res/layout/f_drawer_main.xml
+++ b/app/src/main/res/layout/f_drawer_main.xml
@@ -80,6 +80,14 @@
/>
<se.leap.bitmaskclient.base.views.IconTextEntry
+ android:id="@+id/language_switcher"
+ app:text="@string/select_language"
+ app:icon="@drawable/translate"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:visibility="visible"/>
+
+ <se.leap.bitmaskclient.base.views.IconTextEntry
android:id="@+id/advancedSettings"
app:icon="@drawable/ic_cog"
android:layout_height="wrap_content"
diff --git a/app/src/main/res/layout/f_language_selection.xml b/app/src/main/res/layout/f_language_selection.xml
new file mode 100644
index 00000000..0569c7c1
--- /dev/null
+++ b/app/src/main/res/layout/f_language_selection.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:layout_margin="@dimen/stdpadding"
+ tools:context=".base.fragments.LanguageSelectionFragment">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/languages"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/f_notification_setup.xml b/app/src/main/res/layout/f_notification_setup.xml
deleted file mode 100644
index d9c7d1a3..00000000
--- a/app/src/main/res/layout/f_notification_setup.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:padding="@dimen/stdpadding"
- android:layout_margin="@dimen/activity_margin"
- tools:context=".providersetup.fragments.ProviderSelectionFragment">
-
- <androidx.appcompat.widget.LinearLayoutCompat
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- android:layout_margin="@dimen/activity_margin"
- >
- <androidx.appcompat.widget.AppCompatTextView
- android:id="@+id/tv_title"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textAppearance="@style/TextAppearance.AppCompat.Title"
- android:text="@string/title_upcoming_notifications_request"
- android:paddingBottom="@dimen/stdpadding"
- />
- <androidx.appcompat.widget.AppCompatTextView
- android:id="@+id/tv_circumvention_description"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textAppearance="@style/TextAppearance.AppCompat.Body1"
- android:text="@string/upcoming_notifications_request_description"/>
- </androidx.appcompat.widget.LinearLayoutCompat>
-</ScrollView> \ No newline at end of file
diff --git a/app/src/main/res/layout/f_vpn_permission_setup.xml b/app/src/main/res/layout/f_permission_explanation.xml
index 99dd531b..447f12de 100644
--- a/app/src/main/res/layout/f_vpn_permission_setup.xml
+++ b/app/src/main/res/layout/f_permission_explanation.xml
@@ -21,14 +21,34 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Title"
- android:text="@string/title_upcoming_connection_request"
+ android:text="@string/title_upcoming_request"
android:paddingBottom="@dimen/stdpadding"
/>
<androidx.appcompat.widget.AppCompatTextView
- android:id="@+id/tv_circumvention_description"
+ android:id="@+id/title_upcoming_request_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
- android:text="@string/upcoming_connection_request_description"/>
+ android:text="@string/title_upcoming_request_summary"/>
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/title_upcoming_connection_request_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/stdpadding"
+ android:textAppearance="@style/TextAppearance.AppCompat.Body1"
+ android:visibility="gone"
+ tools:visibility="visible"
+ android:text="@string/title_upcoming_connection_request_summary"/>
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/title_upcoming_notification_request_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/stdpadding"
+ android:visibility="gone"
+ tools:visibility="visible"
+ android:textAppearance="@style/TextAppearance.AppCompat.Body1"
+ android:text="@string/title_upcoming_notification_request_summary"/>
</androidx.appcompat.widget.LinearLayoutCompat>
</ScrollView> \ No newline at end of file
diff --git a/app/src/main/res/layout/f_provider_selection.xml b/app/src/main/res/layout/f_provider_selection.xml
index 48d5bdd3..8ec7e7c0 100644
--- a/app/src/main/res/layout/f_provider_selection.xml
+++ b/app/src/main/res/layout/f_provider_selection.xml
@@ -61,35 +61,69 @@
android:layout_height="wrap_content"
>
</RadioGroup>
- <androidx.appcompat.widget.LinearLayoutCompat
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- android:id="@+id/expandable_detail_container">
- <androidx.appcompat.widget.AppCompatTextView
- android:paddingTop="@dimen/stdpadding"
- android:id="@+id/provider_description"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- tools:text="@string/provider_description_riseup"/>
- <androidx.appcompat.widget.AppCompatEditText
- android:id="@+id/edit_customProvider"
- android:layout_marginVertical="@dimen/stdpadding"
- android:paddingHorizontal="@dimen/stdpadding"
- android:paddingVertical="@dimen/compact_padding"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="@color/white"
- android:hint="https://example.org"
- android:inputType="textWebEditText"
- android:imeOptions="actionDone"
- android:maxLines="1"
- android:textAppearance="@style/TextAppearance.AppCompat.Body1"
- android:textColorHint="@color/black800_transparent"
- />
- </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.cardview.widget.CardView>
+
+ <androidx.appcompat.widget.LinearLayoutCompat
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="@dimen/activity_margin"
+ android:paddingBottom="@dimen/list_view_margin_top"
+ android:background="@color/color_provider_description_background"
+ android:id="@+id/expandable_detail_container">
+ <androidx.appcompat.widget.AppCompatTextView
+ android:paddingTop="@dimen/stdpadding"
+ android:id="@+id/provider_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:text="@string/provider_description_riseup"/>
+ <androidx.appcompat.widget.AppCompatEditText
+ android:id="@+id/edit_customProvider"
+ android:layout_marginVertical="@dimen/stdpadding"
+ android:paddingHorizontal="@dimen/stdpadding"
+ android:paddingVertical="@dimen/compact_padding"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/white"
+ tools:hint="https://example.org"
+ android:imeOptions="actionDone"
+ android:textAppearance="@style/TextAppearance.AppCompat.Body1"
+ android:textColorHint="@color/black800_transparent"
+ />
+ <LinearLayout
+ android:id="@+id/syntax_check"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <androidx.appcompat.widget.AppCompatTextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/syntax_check"/>
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/syntax_check_result"
+ android:paddingStart="@dimen/stdpadding"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:text="Good"/>
+ </LinearLayout>
+ <LinearLayout
+ android:id="@+id/qr_scanner"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingVertical="@dimen/stdpadding"
+ android:orientation="horizontal">
+ <androidx.appcompat.widget.AppCompatButton
+ android:id="@+id/btn_qr_scanner"
+ android:padding="@dimen/stdpadding"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/qr_code_scanner"
+ android:text="@string/qr_scanner_prompt"
+ android:background="?attr/selectableItemBackground"
+ android:gravity="center_vertical"/>
+ </LinearLayout>
+ </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</ScrollView> \ No newline at end of file
diff --git a/app/src/main/res/layout/f_settings.xml b/app/src/main/res/layout/f_settings.xml
index 3ce19797..87a97455 100644
--- a/app/src/main/res/layout/f_settings.xml
+++ b/app/src/main/res/layout/f_settings.xml
@@ -5,8 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
- android:padding="@dimen/stdpadding"
- tools:viewBindingIgnore="true">
+ android:padding="@dimen/stdpadding">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -20,6 +19,15 @@
android:text="@string/vpn_settings"
/>
+ <se.leap.bitmaskclient.base.views.IconSwitchEntry
+ android:id="@+id/prefer_udp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:text="@string/prefer_udp"
+ app:subtitle="@string/prefer_udp_subtitle"
+ app:icon="@drawable/ic_multiple_stop"
+ app:singleLine="false" />
+
<se.leap.bitmaskclient.base.views.IconTextEntry
android:id="@+id/always_on_vpn"
android:layout_width="match_parent"
@@ -27,8 +35,7 @@
app:text="@string/always_on_vpn"
app:subtitle="@string/subtitle_always_on_vpn"
app:icon="@drawable/ic_always_on_36"
- android:visibility="visible"
- />
+ android:visibility="visible" />
<se.leap.bitmaskclient.base.views.IconTextEntry
android:id="@+id/exclude_apps"
@@ -36,18 +43,7 @@
android:layout_height="wrap_content"
app:text="@string/exclude_apps_fragment_title"
app:icon="@drawable/ic_shield_remove_grey600_36dp"
- android:visibility="visible"
- />
-
- <se.leap.bitmaskclient.base.views.IconSwitchEntry
- android:id="@+id/prefer_udp"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:text="@string/prefer_udp"
- app:subtitle="@string/prefer_udp_subtitle"
- app:icon="@drawable/ic_multiple_stop"
- app:singleLine="false"
- />
+ android:visibility="visible" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/circumvention_header"
@@ -55,28 +51,46 @@
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Title"
android:text="@string/censorship_circumvention"
- android:paddingTop="@dimen/activity_margin"
- />
+ android:paddingTop="@dimen/activity_margin" />
+
<se.leap.bitmaskclient.base.views.IconSwitchEntry
- android:id="@+id/bridges_switch"
+ android:id="@+id/bridge_automatic_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- app:text="@string/nav_drawer_obfuscated_connection"
- app:subtitle="@string/nav_drawer_subtitle_obfuscated_connection"
- app:icon="@drawable/ic_bridge_36"
- app:singleLine="false"
- />
+ app:text="@string/automatic_bridge"
+ app:subtitle="@string/automatic_bridge_description"
+ app:icon="@drawable/bridge_automatic"
+ android:visibility="visible"
+ app:singleLine="false" />
- <se.leap.bitmaskclient.base.views.IconSwitchEntry
- android:id="@+id/snowflake_switch"
- android:layout_width="match_parent"
+ <LinearLayout
+ android:id="@+id/bridge_manual_switch_entry"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- app:icon="@drawable/ic_snowflake"
- app:text="@string/use_snowflake"
- app:subtitle="@string/snowflake_description"
- app:singleLine="false"
- android:visibility="gone"
- />
+ android:orientation="horizontal">
+ <se.leap.bitmaskclient.base.views.IconTextEntry
+ android:id="@+id/bridge_manual_switch"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ app:text="@string/manual_bridge"
+ app:subtitle="@string/manual_bridge_description"
+ app:icon="@drawable/bridge_manual"
+ app:singleLine="false" />
+ <View
+ android:layout_width="1px"
+ android:background="@android:color/darker_gray"
+ android:layout_marginHorizontal="@dimen/stdpadding"
+ android:layout_marginVertical="@dimen/stdpadding"
+ android:layout_height="match_parent"/>
+ <androidx.appcompat.widget.SwitchCompat
+ android:id="@+id/bridge_manual_switch_control"
+ android:layout_gravity="center"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingRight="?android:attr/listPreferredItemPaddingRight"/>
+ </LinearLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/experimental_header"
@@ -84,8 +98,7 @@
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Title"
android:text="@string/experimental_features"
- android:paddingTop="@dimen/activity_margin"
- />
+ android:paddingTop="@dimen/activity_margin" />
<se.leap.bitmaskclient.base.views.IconSwitchEntry
diff --git a/app/src/main/res/layout/v_select_text_list_item.xml b/app/src/main/res/layout/v_select_text_list_item.xml
index 47a1f4ad..44e82906 100644
--- a/app/src/main/res/layout/v_select_text_list_item.xml
+++ b/app/src/main/res/layout/v_select_text_list_item.xml
@@ -4,8 +4,7 @@
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:orientation="vertical"
- android:background="?attr/selectableItemBackground"
- tools:viewBindingIgnore="true">
+ android:background="?attr/selectableItemBackground">
<LinearLayout
android:layout_width="match_parent"
diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties
new file mode 100644
index 00000000..63b46f93
--- /dev/null
+++ b/app/src/main/res/resources.properties
@@ -0,0 +1 @@
+unqualifiedResLocale=en \ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 296a7113..c6178979 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="retry">حاول مجدداً</string>
- <string name="repository_url_text">كود المصدر متواجد هنا https://0xacab.org/leap/bitmask_android</string>
+ <string name="repository_url_text">كود المصدر متاح هنا https://0xacab.org/leap/bitmask_android</string>
<string name="leap_tracker">متتبع الأعطال متوافر هنا
https://0xacab.org/leap/bitmask_android/issues</string>
<string name="translation_project_text">يمكنك الترجمة؟ اطلع على مشروع الترجمة الخاص بنا عبر هذا الرابط
@@ -12,7 +12,7 @@ https://www.transifex.com/projects/p/bitmask/</string>
<string name="connection_details">تفاصيل الاتصال</string>
<string name="routes_info">المسارات: %s</string>
<string name="routes_info6">مسارات IPv6: %s</string>
- <string name="error_empty_username">اسم المستخدم ينبغي أن لا يكون فارغا.</string>
+ <string name="error_empty_username">لا ينبغي أن يظل مكان اسم المستخدم فارغاً.</string>
<string name="cert_from_keystore">احصل على شهادة \'%s\' من مستودع مفاتيح</string>
<string name="provider_label">مزود الخدمة:</string>
<string name="provider_label_none">لا مزود خدمة مفعل</string>
@@ -218,6 +218,10 @@ https://www.transifex.com/projects/p/bitmask/</string>
<string name="provider_description_riseup">توفر Riseup أدوات اتصال عبر الإنترنت للأشخاص والمجموعات التي تعمل على التغيير الاجتماعي التحرري. نحن مشروع لخلق بدائل ديمقراطية وممارسة تقرير المصير من خلال التحكم في وسائل الاتصالات الآمنة الخاصة بنا.</string>
<string name="next">التالي</string>
<string name="add_provider_description">يتصل Bitmask بمزودي الخدمات الموثوقين غير المدرجين بشكل علني. أدخل عنوان URL الخاص بمزود الخدمة الخاص بك أدناه.</string>
+ <string name="add_provider_prompt">أدخل عنوان URL الخاص بموفر الخدمة هنا.</string>
+ <string name="invite_code_provider_description">يتيح لك Bitmask التواصل مع مزودي الخدمة عبر رمز أو كود دعوة خاص.</string>
+ <string name="invite_code_provider_prompt">أدخل رمزك هنا.</string>
+ <string name="qr_scanner_prompt">مسح رمز كيو آر</string>
<string name="provider_description_calyx">كاليكس Calyx هي منظمة تعليمية وبحثية غير ربحية مكرسة لدراسة واختبار وتطوير وتنفيذ تكنولوجيا وأدوات الخصوصية لتعزيز حرية الراي وحرية التعبير والمشاركة المدنية وحقوق الخصوصية على الإنترنت وفي صناعة الاتصالات المتنقلة.</string>
<string name="title_circumvention_setup">هل تحتاج إلى تجاوز الرقابة؟</string>
<string name="circumvention_setup_description">إذا كنت تعيش في مكان يخضع فيه الإنترنت للرقابة، فيمكنك استخدام خيارات التحايل على الرقابة للوصول إلى جميع خدمات الإنترنت. هذه الخيارات سوف تبطئ اتصالك!</string>
@@ -236,11 +240,29 @@ https://www.transifex.com/projects/p/bitmask/</string>
<string name="snowflake_broker_success">نجح لقاء وكيل Snowflake</string>
<string name="snowflake_sending_data">إرسال البيانات عبر Snowflake</string>
<string name="title_upcoming_connection_request">طلب اتصال القادم</string>
- <string name="upcoming_connection_request_description">في اللوحة التالية، سيذكرك Android أنه من الضروري أن تثق بمزود VPN الخاص بك. تتعاون Bitmask فقط مع مقدمي الخدمة الذين يلتزمون بأفضل ممارسات الخصوصية الصارمة لشبكات VPN ولديهم سجل يمكن التحقق منه في حماية بيانات المستخدم وهوياته.</string>
<string name="title_upcoming_notifications_request">طلب إشعارات القادمة</string>
- <string name="upcoming_notifications_request_description">في اللوحة التالية، سيسألك Android عما إذا كنت تريد السماح بالإشعارات. سيضمن ذلك اتصالاً مستقرًا في الخلفية ويمكّنك من رؤية استخدامك للبيانات من داخل مركز إشعارات Android.</string>
<string name="title_setup_success">كل شيء جاهز!</string>
<string name="setup_success_description">انقر الزر ادناه للاتصال</string>
<string name="permission_rejected">تم رفض طلب ترخيص.</string>
<string name="login_not_supported">لا يدعم إصدار التطبيق الحالي عمليات تسجيل الدخول، وهو ما تحتاجه لتحديث شهادة VPN الخاصة بك لهذا المزود.</string>
+ <string name="select_language">اختر اللغة</string>
+ <string name="syntax_check">التحقق من البنية:</string>
+ <string name="validation_status_success">جيد</string>
+ <string name="validation_status_failure">سيء</string>
+ <string name="enter_invite_code">أدخل رمز الدعوة</string>
+ <string name="scan_qr_code">مسح رمز كيو آر</string>
+ <string name="invalid_code">الرمز باطل</string>
+ <string name="automatic_bridge">تلقائي (مستحسن)</string>
+ <string name="automatic_bridge_description">ستتم محاولة الاتصال باستخدام أفضل الجسور والبروتوكولات المتاحة.</string>
+ <string name="manual_bridge">تهيئة يدوية</string>
+ <string name="manual_bridge_description">اختر جسور خاصة وبروتوكولات محددة</string>
+ <string name="censorship_circumvention_description">تتطلب التهيئة اليدوية فهماً تقنياً. توخَّ الحذر.</string>
+ <string name="discovery">اكشتاف</string>
+ <string name="discovery_description">يمكن لأجهزة الحجب أن تمنع الكشف عن معلومات التهيئة المهمة الصادرة عن مزودك. استخدم خاصية التحايل لتجاوز الحظر.</string>
+ <string name="automatically_select">اختيار تلقائي</string>
+ <string name="invite_proxy">وكيل دعوة</string>
+ <string name="tunnelling">نفقي</string>
+ <string name="tunnelling_description">يمكن لأجهزة الحجب أن تمنع الوصول إلى الإنترنت المفتوح. استخدم خاصية التحايل لتجاوز الحظر.</string>
+ <string name="port_hopping">القفز بين المنافذ</string>
+ <string name="port_hopping_description">تلجأ أدوات الحجب إلى تحليل حركة المرور لمنع الوصول إلى الإنترنت المفتوح. يمكن للقفز عبر المنافذ أن يصعّب هذه المهمة. </string>
</resources>
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index b12925d7..16cd8d9f 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -1,19 +1,28 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="retry">Opakovat</string>
+ <string name="repository_url_text">Zdrojový kód je dostupný na https://0xacab.org/leap/bitmask_android</string>
+ <string name="leap_tracker">Sledování problémů je dostupné na https://0xacab.org/leap/bitmask_android/issues</string>
+ <string name="translation_project_text">Překlady jsou vítány. Podívejte se na náš projekt Transifex na https://www.transifex.com/projects/p/bitmask/</string>
<string name="switch_provider_menu_option">Přepnout poskytovatele</string>
<string name="info">info</string>
<string name="show_connection_details">Zobrazit detaily spojení</string>
+ <string name="connection_details">Podrobnosti o připojení</string>
<string name="routes_info">Směrování: %s</string>
+ <string name="routes_info6">Trasy IPv6: %s</string>
<string name="error_empty_username">Jméno nesmí být prázdné.</string>
+ <string name="cert_from_keystore">Načten certifikát „%s“ ze systémového úložiště</string>
<string name="provider_label">Poskytovatel</string>
<string name="provider_label_none">Žádný poskytovatel nenakonfigurován.</string>
<string name="status_unknown">Status neznámý</string>
+ <string name="eip_service_label">Přístup k šifrované internetové VPN</string>
<string name="configuration_wizard_title">Vyberte si poskytovatele služeb</string>
<string name="add_provider">Přidejte nového poskytovatele</string>
<string name="introduce_new_provider">Přidejte nového poskytovatele služeb</string>
<string name="save">Uložit</string>
<string name="new_provider_uri">Jméno domény</string>
+ <string name="valid_url_entered">Adresa URL je neplatná</string>
+ <string name="not_valid_url_entered">Poškozená adresa URL</string>
<string name="provider_details_title">Detaily poskytovatele</string>
<string name="use_anonymously_button">Použít anonymně</string>
<string name="username_hint">uživatelské jméno</string>
@@ -24,38 +33,237 @@
<string name="password_mismatch">Zadaná hesla se neshodují</string>
<string name="user_message">Zpráva uživatele</string>
<string name="about_fragment_title">O programu</string>
+ <string name="exclude_apps_fragment_title">Vyloučit aplikace z VPN</string>
+ <string name="error_srp_math_error_user_message">Zkuste to znovu: chyba matematiky serveru</string>
+ <string name="error_bad_user_password_user_message">Nesprávné uživatelské jméno nebo heslo</string>
+ <string name="error_not_valid_password_user_message">Musí být dlouhé alespoň 8 znaků</string>
<string name="error_client_http_user_message">Zkuste znovu: chyba HTTP klienta.</string>
<string name="error_io_exception_user_message">Zkuste znovu: I/O chyba</string>
<string name="error_json_exception_user_message">Zkuste znovu: Špatná odpověď serveru</string>
+ <string name="error_no_such_algorithm_exception_user_message">Šifrovací algoritmus nenalezen. Aktualizujte prosím svůj systém!</string>
+ <string name="signup_or_login_button">Registrovat/přihlásit se</string>
<string name="login_button">Přihlásit</string>
+ <string name="login_to_profile">Přihlásit se k profilu</string>
<string name="logout_button">Odhlásit</string>
<string name="signup_button">Registrovat</string>
+ <string name="create_profile">Vytvořit profil</string>
+ <string name="setup_provider">Nastavit poskytovatele</string>
<string name="setup_error_title">Chyba konfigurace</string>
<string name="setup_error_configure_button">Konfigurovat</string>
<string name="setup_error_close_button">Odejít</string>
+ <string name="setup_error_text">Při nastavování aplikace %s s vaším vybraným poskytovatelem došlo k chybě.\n\nMůžete změnit nastavení nebo aplikaci ukončit a nastavit poskytovatele při dalším spuštění.</string>
+ <string name="setup_error_text_custom">Při nastavování aplikace %s došlo k chybě.\n\nMůžete změnit nastavení nebo aplikaci ukončit.</string>
+ <string name="server_unreachable_message">Server je nedostupný, zkuste to prosím znovu.</string>
+ <string name="error.security.pinnedcertificate">Chyba zabezpečení, aktualizujte aplikaci nebo vyberte jiného poskytovatele.</string>
+ <string name="malformed_url">Zdá se, že nejde o poskytovatele %s.</string>
+ <string name="certificate_error">Toto není důvěryhodný poskytovatel %s.</string>
+ <string name="service_is_down_error">Služba je offline.</string>
<string name="configuring_provider">Konfigurace poskytovatele</string>
+ <string name="incorrectly_downloaded_certificate_message">Váš anonymní certifikát nebyl stažen</string>
+ <string name="downloading_certificate_message">Stahování VPN certifikátu</string>
+ <string name="updating_certificate_message">Aktualizace VPN certifikátu</string>
+ <string name="login.riseup.warning">Uživatelé služby Riseup si budou muset vytvořit samostatný účet, aby mohli službu VPN používat</string>
<string name="succesful_authentication_message">Ověřené</string>
<string name="authentication_failed_message">Chyba ověření</string>
+ <string name="registration_failed_message">Registrace selhala</string>
<string name="eip_status_start_pending">Zahajování připojení</string>
+ <string name="eip_status_connecting">Připojování k VPN</string>
+ <string name="eip_status_unsecured">Nezabezpečené připojení</string>
+ <string name="eip_status_secured">Zabezpečené připojení</string>
<string name="eip_cancel_connect_title">Zrušit připojení?</string>
<string name="eip_cancel_connect_text">Probíhá pokus o přihlášení. Přejete si jej zrušit?</string>
+ <string name="eip.warning.browser_inconsistency">Vypnout připojení k VPN? Když je VPN vypnutá, může dojít k úniku osobních údajů k poskytovateli internetu nebo do místní sítě.</string>
+ <string name="eip_state_not_connected">Neběží! Nezabezpečené připojení!</string>
+ <string name="eip_state_connected">Připojení je zabezpečené</string>
+ <string name="provider_problem">Zdá se, že u poskytovatele došlo k chybě.</string>
+ <string name="try_another_provider">Vyberte prosím jiného poskytovatele, nebo kontaktujte svého.</string>
<string name="default_username">Anonymní</string>
<string name="logging_in">Přihlašování</string>
+ <string name="signing_up">Registrace</string>
+ <string name="vpn.button.turn.on">Zapnout</string>
+ <string name="vpn.button.turn.off">Vypnout</string>
+ <string name="vpn_button_turn_off_blocking">Přestat blokovat</string>
+ <string name="vpn_securely_routed">Váš provoz je bezpečně směrován přes:</string>
+ <string name="vpn_securely_routed_no_internet">Nebylo zjištěno připojení k internetu. Jakmile bude připojení obnoveno, budeme bezpečně směrovat váš provoz přes:</string>
<string name="log_fragment_title">Log</string>
<string name="vpn_fragment_title">VPN</string>
<string name="navigation_drawer_open">Otevřít navigační panel</string>
<string name="navigation_drawer_close">Zavří navigační panel</string>
+ <string name="action_example">Příklad akce</string>
<string name="action_settings">Nastavení</string>
+ <string name="void_vpn_establish">%s blokuje všechen odchozí internetový provoz.</string>
+ <string name="void_vpn_error_establish">Blokování veškerého internetového provozu selhalo.</string>
+ <string name="void_vpn_stopped">Zastaveno blokování veškerého odchozího internetového provozu.</string>
+ <string name="void_vpn_title">Blokování provozu</string>
+ <string name="update_provider_details">Aktualizovat podrobnosti o poskytovateli</string>
+ <string name="update_certificate">Aktualizovat certifikát</string>
+ <string name="warning_eip_json_corrupted">Aktualizace nastavení poskytovatele selhala.</string>
+ <string name="eip_json_corrupted_user_message">Aktualizace nastavení poskytovatele selhala. Přihlaste se a zkuste to znovu.</string>
+ <string name="warning_client_parsing_error_gateways">Nepodařilo se rozpoznat brány poskytovatele. Možná není správně nastaven.</string>
+ <string name="warning_corrupted_provider_details">Uložené informace o poskytovateli jsou poškozené. Můžete buď aktualizovat aplikaci %s (doporučeno) nebo aktualizovat informace o poskytovateli použitím komerčního certifikátu CA.</string>
+ <string name="warning_corrupted_provider_cert">Uložený certifikát poskytovatele je neplatný. Můžete buď aktualizovat aplikaci %s (doporučeno) nebo aktualizovat certifikát poskytovatele použitím komerčního certifikátu CA.</string>
+ <string name="warning_expired_provider_cert">Uložený certifikát poskytovatele vypršel. Můžete buď aktualizovat aplikaci %s (doporučeno) nebo aktualizovat certifikát poskytovatele použitím komerčního certifikátu CA.</string>
+ <string name="downloading_vpn_certificate_failed">Stahování certifikátu VPN selhalo. Zkuste to znovu nebo vyberte jiného poskytovatele.</string>
+ <string name="vpn_certificate_is_invalid">Je čas aktualizovat váš certifikát VPN. Stáhněte si nový certifikát, aby bylo vaše připojení bezpečné. Jedná se o běžnou aktualizaci.</string>
+ <string name="vpn_certificate_user_message">Certifikát VPN je neplatný. Přihlaste se pro stažení nového.</string>
+ <string name="save_battery">Spořič baterie</string>
+ <string name="subtitle_save_battery">Zakázáno při zapnutém VPN hotspotu</string>
+ <string name="save_battery_message">Datová připojení na pozací budou hibernovat, když je váš telefon neaktivní.</string>
+ <string name="always_on_vpn">Vždy zapnutá VPN</string>
+ <string name="subtitle_always_on_vpn">Otevřít nastavení systému Android</string>
+ <string name="tethering">VPN hotspot</string>
+ <string name="ipv6Firewall">Blokovat IPv6</string>
+ <string name="require_root">Vyžaduje oprávnění root</string>
+ <string name="show_experimental">Zobrazit experimentální funkce</string>
+ <string name="hide_experimental">Skrýt experimentální funkce</string>
+ <string name="experimental_features">Experimentální funkce</string>
+ <string name="tethering_enabled_message">Nejprve prosím povolte tethering v <![CDATA[<b>systémových nastaveních</b>]]>.</string>
+ <string name="tethering_message">Sdílejte svou VPN s ostatními zařízeními přes:</string>
+ <string name="tethering_wifi">Wi-Fi hotspot</string>
+ <string name="tethering_usb">USB tethering</string>
+ <string name="tethering_bluetooth">Bluetooth tethering</string>
+ <string name="do_not_show_again">Nezobrazovat znovu</string>
+ <string name="always_on_vpn_user_message">Chcete-li v nastavení sítě VPN pro Android povolit funkci trvalé VPN, klepněte na ikonu konfigurace [img src] a přepínač zapněte.</string>
+ <string name="always_on_blocking_vpn_user_message">Chcete-li optimálně chránit své soukromí, měli byste také aktivovat možnost „Blokovat připojení bez VPN“.</string>
<string name="donate_title">Darujte</string>
+ <string name="donate_default_message">Pokud si vážíte bezpečné komunikace, která je snadná jak pro koncového uživatele, tak pro poskytovatele služeb, přispějte prosím ještě dnes.</string>
+ <string name="donate_message">LEAP je závislý na darech a grantech. Pokud si vážíte bezpečné komunikace, která je snadná jak pro koncového uživatele, tak pro poskytovatele služeb, přispějte prosím ještě dnes.</string>
+ <string name="donate_button_remind_later">Připomenout později</string>
<string name="donate_button_donate">Darujte</string>
+ <string name="obfuscated_connection">Používám maskované připojení.</string>
+ <string name="obfuscated_connection_try">Pokus o maskované připojení.</string>
<string name="nav_drawer_obfuscated_connection">Použít bridge</string>
+ <string name="nav_drawer_subtitle_obfuscated_connection">Obejít filtrování VPN</string>
+ <string name="warning_exclude_apps_message">Dávejte pozor na vyloučení aplikací ze sítě VPN. Odhalíte tak svou identitu a ohrozíte svou bezpečnost.</string>
+ <plurals name="subtitle_exclude_apps">
+ <item quantity="one">%d nechráněná aplikace</item>
+ <item quantity="few">%d nechráněné aplikace</item>
+ <item quantity="many">%d nechráněných aplikací</item>
+ <item quantity="other">%d nechráněných aplikací</item>
+ </plurals>
+ <string name="warning_no_more_gateways_use_pt">Aplikaci %s se nepodařilo připojit. Je možné, že připojení VPN jsou blokována. Chcete se zkusit připojit pomocí maskovaného připojení?</string>
+ <string name="warning_no_more_gateways_no_pt">Aplikaci %s se nepodařilo připojit. Chcete to zkusit znovu?</string>
+ <string name="warning_no_more_gateways_use_ovpn">Aplikaci %s se nepodařilo připojit pomocí maskovaného připojení. Chcete se zkusit připojit pomocí standardní VPN?</string>
+ <string name="warning_no_more_gateways_manual_gw_selection">Aplikaci %1$s se nepodařilo připojit k %2$s. Chcete se zkusit automaticky připojit k nejlepší lokaci?</string>
+ <string name="warning_option_try_best">Vyzkoušet nejlepší lokaci</string>
+ <string name="warning_option_try_pt">Vyzkoušet maskované připojení</string>
+ <string name="warning_option_try_ovpn">Vyzkoušet standardní připojení</string>
+ <string name="vpn_error_establish">Systému Android se nepodařilo navázat službu VPN.</string>
+ <string name="root_permission_error">Aplikace %s nedokáže bez oprávnění root vykonávat funkce, jako je VPN hotspot nebo IPv6 firewall.</string>
+ <string name="qs_enable_vpn">Spustit %s</string>
+ <string name="version_update_found">Klepněte sem pro zahájení stahování.</string>
+ <string name="version_update_title">Byla nalezena nová verze aplikace %s.</string>
+ <string name="version_update_apk_description">Stahování nové verze aplikace %s</string>
+ <string name="version_update_download_title">Byla stažena nová verze aplikace %s.-</string>
+ <string name="version_update_download_description">Klepněte sem pro instalaci aktualizace.</string>
+ <string name="version_update_error_pgp_verification">Chyba ověření PGP. Ignoruji stahování.</string>
+ <string name="version_update_error">Aktualizace selhala.</string>
+ <string name="version_update_error_permissions">Chybějící oprávnění k instalaci aplikace.</string>
+ <string name="gateway_selection_title">Vyberte lokaci</string>
+ <string name="gateway_selection_recommended_location">Doporučená lokace</string>
+ <string name="gateway_selection_recommended">Doporučená</string>
+ <string name="gateway_selection_manually">Vybrat ručně</string>
+ <string name="gateway_selection_automatic_location">Automaticky použít nejlepší připojení</string>
<string name="gateway_selection_automatic">Automaticky</string>
+ <string name="reconnecting">Obnovuji připojení...</string>
+ <string name="tor_starting">Spouštění mostů pro obcházení cenzury...</string>
+ <string name="tor_stopping">Zastavování mostů</string>
+ <string name="tor_started">Používám mosty pro obcházení cenzury</string>
+ <string name="log_conn_done_pt">Připojeno k zásuvnému transportu</string>
+ <string name="log_conn_pt">Připojování k zásuvnému transportu</string>
+ <string name="log_conn_done">Připojeno k uzlu</string>
+ <string name="log_handshake">Vyjednávání spojení s uzlem</string>
+ <string name="log_handshake_done">Spojení s uzlem vyjednáno</string>
<string name="log_onehop_create">Spojování s šifrovaným adresářem</string>
+ <string name="log_requesting_status">Žádost o souhlas se stavem sítě</string>
+ <string name="log_loading_status">Načítání souhlasu se stavem sítě</string>
<string name="log_loading_keys">Nahrávání certifikátů autorit</string>
+ <string name="log_requesting_descriptors">Žádost o popisy uzlu</string>
+ <string name="log_loading_descriptors">Načítání popisů uzlu</string>
+ <string name="log_enough_dirinfo">Načteno dostatečné množství informací o adresářích pro sestavení obvodů</string>
+ <string name="log_ap_handshake_done">Vyjednávání s uzlem pro sestavování obvodů dokončeno</string>
<string name="log_circuit_create">Navazování okruhu Tor</string>
<string name="log_done">Běží</string>
+ <string name="channel_name_tor_service">Služba mostů %s</string>
+ <string name="channel_description_tor_service">Informuje o využití mostů během konfigurace %s.</string>
+ <string name="error_tor_timeout">Spuštění mostů selhalo. Chcete pokus opakovat nebo pokračovat bez maskovaného bezpečného spojení pro nastavení aplikace %s?</string>
+ <string name="retry_unobfuscated">Opakovat pokus bez maskování</string>
<string name="hide">Skrýt</string>
+ <string name="error_network_connection">Aplikace %s nemá připojení k internetu. Zkontrolujte prosím nastavení Wi-Fi a mobilních dat.</string>
+ <string name="censorship_circumvention">Obcházení cenzury</string>
<string name="use_snowflake">Použít Snowflake</string>
+ <string name="snowflake_description">Chránit proces konfigurace před cenzurou.</string>
+ <string name="vpn_settings">Nastavení VPN</string>
+ <string name="prefer_udp">Použít UDP, pokud je dostupné</string>
+ <string name="prefer_udp_subtitle">UDP může být rychlejší a lepší pro streamování, nefunguje ale u všech sítí.</string>
+ <string name="disabled_while_bridges_on">Zakázáno při používání mostů.</string>
+ <string name="hint_bridges">V současné době lze vybrat pouze lokace podporující mosty.</string>
+ <string name="option_disable_bridges">Zakázat mosty</string>
+ <string name="eip_state_insecure">Připojení není zabezpečené</string>
+ <string name="connection_not_connected">Může dojít k úniku informací k poskytovateli internetu nebo do místní sítě.</string>
+ <string name="eip_state_no_network">Nemáte funkční připojení k internetu. Jakmile ho získáte zpět, budete automaticky připojeni k</string>
+ <string name="eip_state_blocking">%1$s blokuje veškerý síťový provoz.</string>
+ <string name="disabled_while_udp_on">Zakázáno při zapnutém UDP.</string>
<string name="advanced_settings">Rozšířené nastavení</string>
<string name="cancel_connection">Odpojit</string>
+ <string name="unknown_location">Neznámá lokace</string>
+ <string name="splash_footer">Vyvinuto v projektu LEAP</string>
+ <string name="welcome">Vítejte!</string>
+ <string name="select_provider">Vyberte svého poskytovatele</string>
+ <string name="select_provider_description">Při používání sítě VPN přenášíte svou důvěru z poskytovatele internetových služeb na poskytovatele sítě VPN. Bitmask se připojuje pouze k poskytovatelům s jasnou historií ochrany soukromí.</string>
+ <string name="provider_description_riseup">Riseup poskytuje online komunikační nástroje pro lidi a skupiny pracující na svobodné sociální změně. Jsme projekt, jehož cílem je vytvářet demokratické alternativy a praktikovat sebeurčení prostřednictvím kontroly vlastních bezpečných komunikačních prostředků.</string>
+ <string name="next">Další</string>
+ <string name="add_provider_description">Bitmask umožňuje připojení k poskytovatelům, kteří nejsou veřejně uvedeni. Ujistěte se, že poskytovatele, kterého přidáváte, znáte a důvěřujete mu.</string>
+ <string name="add_provider_prompt">Sem zadejte adresu URL poskytovatele.</string>
+ <string name="invite_code_provider_description">Bitmask umožňuje připojení k poskytovatelům pomocí soukromého zvacího kódu.</string>
+ <string name="invite_code_provider_prompt">Sem zadejte váš důvěryhodný zvací kód.</string>
+ <string name="qr_scanner_prompt">Naskenovat QR kód</string>
+ <string name="provider_description_calyx">Calyx je nezisková vzdělávací a výzkumná organizace, která se věnuje studiu, testování, vývoji a zavádění technologií a nástrojů pro ochranu soukromí s cílem podpořit svobodu slova, svobodu projevu, občanskou angažovanost a práva na ochranu soukromí na internetu a v odvětví mobilních komunikací.</string>
+ <string name="title_circumvention_setup">Vyžadujete obcházení cenzury?</string>
+ <string name="circumvention_setup_description">Pokud žijete v místě, kde je internet cenzurován, můžete využít naše možnosti obcházení cenzury a získat přístup ke všem internetovým službám. Tyto možnosti zpomalí vaše připojení!</string>
+ <string name="circumvention_setup_hint">%s se automaticky pokusí připojit k internetu pomocí různých technologií obcházení. Tuto funkci můžete jemně doladit v pokročilém nastavení.</string>
+ <string name="use_standard_vpn">Použít standardní %s</string>
+ <string name="use_circumvention_tech">Použít technologii obcházení (pomalejší)</string>
+ <string name="description_configure_provider">Pro připojení k poskytovateli %1$s je třeba načíst všechny požadované konfigurační informace. K tomu dochází pouze při prvním nastavení.</string>
+ <string name="description_configure_provider_circumvention">%1$s se pokouší shromáždit od zprostředkovatele všechna požadovaná konfigurační data. K tomu dochází pouze při prvním nastavení. Zvolili jste použití technologie obcházení, takže to může nějakou dobu trvat.</string>
+ <string name="details">Detaily</string>
+ <string name="tor_status">Stav sítě Tor</string>
+ <string name="snowflake_status">Stav Snowflake</string>
+ <string name="snowflake_started">Klient Snowflake spuštěn</string>
+ <string name="snowflake_negotiating_rendezvous_http">Vyjednávání setkání s proxy serverem Snowflake (http)</string>
+ <string name="snowflake_negotiating_rendezvous_amp_cache">Vyjednávání setkání s proxy serverem Snowflake (amp cache)</string>
+ <string name="snowflake_socks_error">Chyba Snowflake SOCKS</string>
+ <string name="snowflake_broker_success">Úspěšné setkání s proxy serverem Snowflake</string>
+ <string name="snowflake_sending_data">Posílání dat přes Snowflake</string>
+ <string name="title_upcoming_connection_request">Nadcházející žádost o připojení</string>
+ <string name="title_upcoming_request">Nadcházející požadavky</string>
+ <string name="title_upcoming_request_summary">V dalších panelech vás systém Android požádá o oprávnění ve formě žádosti o připojení a žádosti o oznámení.</string>
+ <string name="title_upcoming_connection_request_summary_custom">Přijetí požadavku na připojení je nezbytné pro používání základních funkcí aplikace %s.</string>
+ <string name="title_upcoming_connection_request_summary">Pokud jde o žádost o připojení, je důležité vědět, že Bitmask spolupracuje pouze s důvěryhodnými partnerskými poskytovateli, kteří dodržují osvědčené postupy pro sítě VPN a mají ověřitelnou historii ochrany dat a identit uživatelů. Pokud se však ručně připojujete k neveřejnému poskytovateli, ujistěte se, že mu důvěřujete.</string>
+ <string name="title_upcoming_notification_request_summary">Přijetí požadavku na oznámení umožní aplikaci běžet na pozadí a zobrazit využití dat v centru oznámení systému Android.</string>
+ <string name="title_upcoming_notifications_request">Nadcházející žádost o oznámení</string>
+ <string name="title_setup_success">Jste připraveni!</string>
+ <string name="setup_success_description">Klepněte na tlačítko níže pro připojení</string>
+ <string name="permission_rejected">Žádost o oprávnění zamítnuta.</string>
+ <string name="login_not_supported">Aktuální verze aplikace nepodporuje přihlašování, proto je třeba aktualizovat certifikát VPN pro tohoto poskytovatele.</string>
+ <string name="select_language">Vybrat jazyk</string>
+ <string name="syntax_check">Kontrola syntaxe:</string>
+ <string name="validation_status_success">Dobrá</string>
+ <string name="validation_status_failure">Špatná</string>
+ <string name="enter_invite_code">Zadejte kód pozvánky</string>
+ <string name="scan_qr_code">Naskenovat QR kód</string>
+ <string name="invalid_code">Neplatný kód</string>
+ <string name="automatic_bridge">Automaticky (doporučeno)</string>
+ <string name="automatic_bridge_description">O připojení se pokusíme pomocí nejlepších dostupných mostů a protokolů.</string>
+ <string name="manual_bridge">Ruční nastavení</string>
+ <string name="manual_bridge_description">Vybrat soukromé mosty a specifické protokoly</string>
+ <string name="censorship_circumvention_description">Ruční nastavení vyžaduje technické znalosti. Postupujte opatrně.</string>
+ <string name="discovery">Objevování</string>
+ <string name="discovery_description">Cenzoři mohou blokovat zjišťování kritických informací o konfiguraci od vašeho poskytovatele. Chcete-li blokování obejít, zvolte možnost obcházení.</string>
+ <string name="automatically_select">Automaticky vybrat</string>
+ <string name="invite_proxy">Proxy pozvánek</string>
+ <string name="tunnelling">Tunelování</string>
+ <string name="tunnelling_description">Cenzoři mohou blokovat přístup k otevřenému internetu. Chcete-li blokování obejít, zvolte možnost obcházení.</string>
+ <string name="port_hopping">Přeskakování portů</string>
+ <string name="port_hopping_description">Cenzoři používají analýzu provozu k blokování přístupu k otevřenému internetu. Přeskakování portů jim to může ztížit.</string>
</resources>
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index a339be9d..0d3c80a2 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -104,7 +104,7 @@
<string name="warning_corrupted_provider_cert">Das gespeicherte Providerzertifikat ist ungültig. Du kannst entweder %s aktualisieren (Empfehlung) oder das Providerzertifikat mithilfe eines kommerziellen CA Zertifikates aktualisieren. </string>
<string name="warning_expired_provider_cert">Das gespeicherte Providerzertifikat ist abgelaufen. Du kannst entweder %s aktualisieren (Empfehlung) oder das Providerzertifikat mithilfe eines kommerziellen CA Zertifikates aktualisieren. </string>
<string name="downloading_vpn_certificate_failed">Das Herunterladen des VPN Zertifikates ist fehlgeschlagen. Versuche es erneut oder wähle einen anderen Provider aus.</string>
- <string name="vpn_certificate_is_invalid">Das VPN Zertifikat ist ungültig. Versuche ein neues herunterzuladen.</string>
+ <string name="vpn_certificate_is_invalid">Es ist Zeit, dein VPN Zertifikat zu aktualisieren. Lade ein neues herunter, um dich weiterhin sicher zu verbinden. Dies ist ein Routineaktualisierung.</string>
<string name="vpn_certificate_user_message">Das VPN Zertifikat ist ungültig. Bitte melde dich an, um ein neues herunterzuladen.</string>
<string name="save_battery">Batteriesparmodus</string>
<string name="subtitle_save_battery">Deaktiviert während der VPN Hotspot an ist</string>
@@ -209,10 +209,14 @@
<string name="welcome">Willkommen!</string>
<string name="select_provider">Wähle deinen Provider</string>
<string name="select_provider_description">Wenn du ein VPN benutzt, überträgst du dein Vertrauen von deinem Internet Service Provider auf deinen VPN-Anbieter. Bitmask verbindet sich nur mit VPN Providern, die in der Vergangenheit bewiesen haben, dass sie für einen guten Datenschutz einstehen.</string>
- <string name="provider_description_riseup">Riseup bietet Online-Kommunikationstools für Menschen und Gruppen, die an befreiendem gesellschaftlichem Wandel arbeiten. Wir sind ein Projekt, das demokratische Alternativen entwickelt und wir üben Selbstbestimmung aus, indem wir unsere eigenen sicheren Kommunikationswege kontrollieren. </string>
+ <string name="provider_description_riseup">Riseup bietet Online-Kommunikationswerkzeuge für Menschen und Gruppen, die sich für einen progressiven sozialen Wandel einsetzen. Wir sind ein Projekt zur Verwirklichung demokratischer Alternativen und ermöglichen mehr Selbstbestimmung, indem wir eigene sichere Kommunikationsmittel bereitstellen.</string>
<string name="next">Weiter</string>
- <string name="add_provider_description">Bitmask verbindet sich mit vertrauenswürdigen Providern, die nicht öffentlich gelistet sind. Gib die URL deines Providers hier an.</string>
- <string name="provider_description_calyx">Calyx ist eine gemeinnützige Bildungs- und Forschungsorganisation, die sich dem Studium, der Erprobung, der Entwicklung und der Umsetzung von Technologien und Instrumenten zum Schutz der Privatsphäre widmet, um die Redefreiheit, die freie Meinungsäußerung, bürgerliches Engagement und Datenschutzrechte im Internet und in der Mobilfunkbranche zu fördern.</string>
+ <string name="add_provider_description">Bitmask ermöglicht es, sich mit Anbietern zu verbinden, die nicht öffentlich gelistet sind. Stelle sicher, dass du den Anbieter kennst und ihm vertraust, wenn du ihn hinzufügst!</string>
+ <string name="add_provider_prompt">Gibt die Anbieter URL hier ein.</string>
+ <string name="invite_code_provider_description">Bitmask ermöglicht es dir, dich mittels Einladungscodes mit einem VPN-Anbieter zu verbinden.</string>
+ <string name="invite_code_provider_prompt">Gibt deinen Einladungscode hier ein.</string>
+ <string name="qr_scanner_prompt">Lese QR Code</string>
+ <string name="provider_description_calyx">Calyx ist eine gemeinnützige Bildungs- und Forschungsorganisation, die sich dem Studium, der Erprobung, Entwicklung und Umsetzung von Technologien und Instrumenten zum Schutz der Privatsphäre widmet, um Rede- und Meinungsfreiheit, zivilgesellschaftliches Engagement und Datenschutzrechte im Internet und in der Mobilfunkbranche zu fördern.</string>
<string name="title_circumvention_setup">Benötigst du die Umgehung von Zensur?</string>
<string name="circumvention_setup_description">Wenn du an einem Ort lebst, wo das Internet zensiert wird, kannst du unsere Optionen zur Umgehung der Zensur nutzen, um auf alle Internetdienste zuzugreifen. Diese Optionen werden deine Verbindung verlangsamen!</string>
<string name="circumvention_setup_hint">%s versucht automatisch, dich mithilfe einer Reihe von Umgehungstechnologien mit dem Internet zu verbinden. Du kannst dies in den erweiterten Einstellungen genau anpassen.</string>
@@ -230,11 +234,34 @@
<string name="snowflake_broker_success">Snowflake-Proxy-Rendezvous erfolgreich</string>
<string name="snowflake_sending_data">Sende Daten über Snowflake</string>
<string name="title_upcoming_connection_request">Bevorstehende Verbindungsanfrage</string>
- <string name="upcoming_connection_request_description">Im nächsten Panel wird Android Sie daran erinnern, dass es wichtig ist, Ihrem VPN-Anbieter zu vertrauen. Bitmask arbeitet nur mit Anbietern zusammen, die sich an strenge Datenschutzbestimmungen für VPNs halten und nachweislich die Daten und Identitäten der Nutzenden schützen.</string>
+ <string name="title_upcoming_request">Anstehende Anfragen</string>
+ <string name="title_upcoming_request_summary">In den nächsten Schritten fragt dich Android um die Erlaubnis für die VPN-Nutzung und für das Anzeigen von Benachrichtigungen.</string>
+ <string name="title_upcoming_connection_request_summary_custom">Das Akzeptieren der Verbindungsanfrage ist wichtig, um die Kernfunktionalität von %s ausführen zu können.</string>
+ <string name="title_upcoming_connection_request_summary">In Hinsicht auf die Verbindungsanfrage ist es wichtig zu wissen, dass Bitmask nur mit vertrauenswürdigen Anbietern zusammenarbeitet, die sich an bewährte Praktiken für VPNs halten und nachweislich die Daten und Identitäten der Benutzer*innen schützen. Wenn du dich jedoch manuell mit einem nicht-öffentlichen Anbieter verbindest, stelle sicher, dass du diesem vertraust.</string>
+ <string name="title_upcoming_notification_request_summary">Das Akzeptieren der Benachrichtungsanfrage ermöglicht der App im Hintergrund zu laufen und erlaubt es dir den Datenverbrauch im Benachrichtungscenter zu sehen.</string>
<string name="title_upcoming_notifications_request">Bevorstehende Benachrichtigungsanfrage</string>
- <string name="upcoming_notifications_request_description">Im nächsten Fenster wirst du von Android gefragt, ob du Benachrichtigungen zulassen möchtest. Dies gewährleistet eine stabile Hintergrundverbindung und ermöglicht es dir, deine Datennutzung in der Benachrichtigungszentrale von Android zu sehen.</string>
<string name="title_setup_success">Du bist startklar!</string>
<string name="setup_success_description">Klicke auf die Schaltfläche unten, um eine Verbindung herzustellen</string>
<string name="permission_rejected">Rechteanfrage abgelehnt.</string>
<string name="login_not_supported">Die aktuelle App-Version unterstützt keine Logins, was du benötigst, um dein VPN-Zertifikat für diesen Anbieter zu aktualisieren.</string>
+ <string name="select_language">Sprache auswählen</string>
+ <string name="syntax_check">Syntax-Überprüfung:</string>
+ <string name="validation_status_success">Gut</string>
+ <string name="validation_status_failure">ungültig</string>
+ <string name="enter_invite_code">Einladungscode eingeben</string>
+ <string name="scan_qr_code">Lese QR Code</string>
+ <string name="invalid_code">Ungültiger Code</string>
+ <string name="automatic_bridge">Automatisch (empfohlen)</string>
+ <string name="automatic_bridge_description">Die Verbindung wird mit den besten verfügbaren Brücken und Protokollen versucht.</string>
+ <string name="manual_bridge">Manuelle Konfiguration</string>
+ <string name="manual_bridge_description">Wähle private Brücken und spezifische Protokolle aus</string>
+ <string name="censorship_circumvention_description">Die manuelle Konfiguration verlangt technisches Verständnis. Gehe mit Bedacht vor!</string>
+ <string name="discovery">Provider-Erkennung</string>
+ <string name="discovery_description">Zensoren können den Konfigurationsprozess mit deinem Provider blockieren. Wähle eine der Optionen, um die Sperren zu umgehen.</string>
+ <string name="automatically_select">Automatisch auswählen</string>
+ <string name="invite_proxy">Einladungsproxy</string>
+ <string name="tunnelling">Tunnelaufbau</string>
+ <string name="tunnelling_description">Zensoren können den Zugang zum offenen Internet blockieren. Wähle eine der Optionen, um solche Sperren zu umgehen.</string>
+ <string name="port_hopping">Springen über verschiedene Ports</string>
+ <string name="port_hopping_description">Zensoren nutzen die Analyse des Datenverkehrs, um den Zugang zum offenen Internet zu blockieren. Das Springen über verschiedene Ports kann dies für sie schwieriger machen. </string>
</resources>
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 33356e97..7c228f65 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -212,6 +212,10 @@
<string name="provider_description_riseup">Το Riseup παρέχει διαδικτυακά εργαλεία επικοινωνίας για άτομα και ομάδες που εργάζονται για την απελευθερωτική κοινωνική αλλαγή. Είμαστε ένα έργο για τη δημιουργία δημοκρατικών εναλλακτικών λύσεων και την πρακτική της αυτοδιάθεσης ελέγχοντας τα δικά μας ασφαλή μέσα επικοινωνίας.</string>
<string name="next">Επόμενο</string>
<string name="add_provider_description">Το Bitmask συνδέεται με αξιόπιστους παρόχους που δεν αναφέρονται δημόσια. Εισαγάγετε τη διεύθυνση url του παρόχου σας παρακάτω.</string>
+ <string name="add_provider_prompt">Εισάγετε τη διεύθυνση URL του παρόχου εδώ.</string>
+ <string name="invite_code_provider_description">Το Bitmask σάς επιτρέπει να συνδεθείτε με παρόχους χρησιμοποιώντας έναν ιδιωτικό Κωδικό Πρόσκλησης.</string>
+ <string name="invite_code_provider_prompt">Εισάγετε τον αξιόπιστο κωδικό πρόσκλησής σας εδώ.</string>
+ <string name="qr_scanner_prompt">Σκανάρισμα κωδικού QR</string>
<string name="provider_description_calyx">Το Calyx είναι ένας μη κερδοσκοπικός οργανισμός εκπαίδευσης και έρευνας που ασχολείται με τη μελέτη, τη δοκιμή, την ανάπτυξη και την εφαρμογή τεχνολογίας και εργαλείων προστασίας της ιδιωτικής ζωής για την προώθηση της ελευθερίας του λόγου, της ελεύθερης έκφρασης, της συμμετοχής των πολιτών και των δικαιωμάτων ιδιωτικότητας στο διαδίκτυο και στον κλάδο των κινητών επικοινωνιών.</string>
<string name="title_circumvention_setup">Απαιτείτε παράκαμψη λογοκρισίας;</string>
<string name="circumvention_setup_description">Εάν ζείτε όπου το διαδίκτυο είναι λογοκριμένο, μπορείτε να χρησιμοποιήσετε τις επιλογές παράκαμψης λογοκρισίας για πρόσβαση σε όλες τις υπηρεσίες διαδικτύου. Αυτές οι επιλογές θα επιβραδύνουν τη σύνδεσή σας!</string>
@@ -230,11 +234,16 @@
<string name="snowflake_broker_success">Επιτυχές ραντεβού διακομιστή Snowflake</string>
<string name="snowflake_sending_data">Αποστολή δεδομένων μέσω Snowflake</string>
<string name="title_upcoming_connection_request">Επικείμενο αίτημα σύνδεσης</string>
- <string name="upcoming_connection_request_description">Στον επόμενο πίνακα, το Android θα σας υπενθυμίσει ότι είναι απαραίτητο να εμπιστεύεστε τον πάροχο VPN. Το Bitmask συνεργάζεται μόνο με παρόχους που τηρούν αυστηρές βέλτιστες πρακτικές απορρήτου για VPN και έχουν επαληθεύσιμο ιστορικό προστασίας των δεδομένων και των ταυτοτήτων των χρηστών.</string>
<string name="title_upcoming_notifications_request">Αίτημα για επερχόμενες ειδοποιήσεις</string>
- <string name="upcoming_notifications_request_description">Στον επόμενο πίνακα, το Android θα σας ρωτήσει εάν θέλετε να επιτρέψετε τις ειδοποιήσεις. Αυτό θα εξασφαλίσει μια σταθερή σύνδεση στο παρασκήνιο και θα σας επιτρέψει να δείτε τη χρήση των δεδομένων σας μέσα από το κέντρο ειδοποιήσεων του Android.</string>
<string name="title_setup_success">Είστε έτοιμος!</string>
<string name="setup_success_description">Κάντε κλικ στο κουμπί παρακάτω για να συνδεθείτε</string>
<string name="permission_rejected">Το αίτημα άδειας απορρίφθηκε.</string>
<string name="login_not_supported">Η τρέχουσα έκδοση της εφαρμογής δεν υποστηρίζει συνδέσεις, τις οποίες χρειάζεστε για να ενημερώσετε το πιστοποιητικό VPN για αυτόν τον πάροχο.</string>
+ <string name="select_language">Επιλογή Γλώσσας</string>
+ <string name="syntax_check">Έλεγχος σύνταξης:</string>
+ <string name="validation_status_success">Ωραία</string>
+ <string name="validation_status_failure">Κακή</string>
+ <string name="enter_invite_code">Εισάγετε κωδικό πρόσκλησης</string>
+ <string name="scan_qr_code">Σκανάρισμα κωδικού QR</string>
+ <string name="invalid_code">Μη έγκυρος κωδικός</string>
</resources>
diff --git a/app/src/main/res/values-es-rAR/strings.xml b/app/src/main/res/values-es-rAR/strings.xml
index 88cbd8e0..14ecfe95 100644
--- a/app/src/main/res/values-es-rAR/strings.xml
+++ b/app/src/main/res/values-es-rAR/strings.xml
@@ -205,4 +205,5 @@
<string name="next">Siguiente</string>
<string name="details">Detalles</string>
<string name="tor_status">Estado de Tor</string>
+ <string name="select_language">Seleccionar idioma</string>
</resources>
diff --git a/app/src/main/res/values-es-rCU/strings.xml b/app/src/main/res/values-es-rCU/strings.xml
index 8f582320..2fb56504 100644
--- a/app/src/main/res/values-es-rCU/strings.xml
+++ b/app/src/main/res/values-es-rCU/strings.xml
@@ -219,8 +219,8 @@
<string name="circumvention_setup_hint">%s tratará de conectarte a Internet usando varias tecnologías de evasión. Puedes modificar algunas opciones en la configuración avanzada.</string>
<string name="use_standard_vpn">Utiliza el estándar %s</string>
<string name="use_circumvention_tech">Usa tecnología de evasión (más lento)</string>
- <string name="description_configure_provider">Para conectarte a tu proveedor, %1$s está obteniendo toda la información de configuración requerida. Esto solo sucede durante la primera configuración. </string>
- <string name="description_configure_provider_circumvention">%1$s está intentando recopilar toda la información de tu proveedor. Esto solo sucede durante la primera configuración y podría demorar algún tiempo.</string>
+ <string name="description_configure_provider">Para conectarte a tu proveedor, %1$s está obteniendo toda la información de configuración requerida. Esto solo ocurrirá durante la primera configuración. </string>
+ <string name="description_configure_provider_circumvention">%1$s está intentando obtener toda la información de tu proveedor. Esto solo ocurrirá durante la primera configuración y podría demorar algún tiempo.</string>
<string name="details">Detalles</string>
<string name="tor_status">Estado de Tor</string>
<string name="snowflake_status">Estado Snowflake</string>
@@ -231,9 +231,7 @@
<string name="snowflake_broker_success">Conexión del proxy Snowflake exitosa.</string>
<string name="snowflake_sending_data">Enviando datos a través de Snowflake</string>
<string name="title_upcoming_connection_request">Próxima Solicitud de Conexión</string>
- <string name="upcoming_connection_request_description">La siguiente ventana de Android te recordará que es esencial confiar en tu proveedor de VPN. Bitmask solo se asocia con proveedores que siguen estrictas prácticas de privacidad para las VPN y tienen un historial verificable de protección de los datos e identidades de usuarios.</string>
<string name="title_upcoming_notifications_request">Solicitud de Próximas Notificaciones</string>
- <string name="upcoming_notifications_request_description">La próxima ventana de Android te preguntará si quieres permitir notificaciones. Esto garantizará una conexión estable en el background y podrás ver el uso de tus datos en el panel de notificaciones de Android.</string>
<string name="title_setup_success">¡Estás listo! </string>
<string name="setup_success_description">Haz click en el botón de abajo para conectar.</string>
<string name="permission_rejected">Permiso rechazado.</string>
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 2fd995ab..039e553e 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -213,6 +213,7 @@
<string name="provider_description_riseup">Riseup proporciona herramientas de comunicación en línea para personas y grupos que trabajan por un cambio social liberador. Somos un proyecto para crear alternativas democráticas y practicar la autodeterminación controlando nuestros propios medios de comunicación seguros.</string>
<string name="next">Siguiente</string>
<string name="add_provider_description">Bitmask se conecta a proveedores confiables que no figuran en la lista pública. Ingrese la URL de su proveedor a continuación.</string>
+ <string name="qr_scanner_prompt">Escanear código QR</string>
<string name="provider_description_calyx">Calyx es una organización de educación e investigación sin fines de lucro dedicada a estudiar, probar, desarrollar e implementar tecnologías y herramientas de privacidad para promover la libertad de expresión, el compromiso cívico y los derechos de privacidad en Internet y en la industria de las comunicaciones móviles.</string>
<string name="title_circumvention_setup">¿Necesita eludir la Censura?</string>
<string name="circumvention_setup_description">Si vive en un lugar donde Internet está censurado, puede utilizar nuestras opciones de elusión de la censura para acceder a todos los servicios de Internet. ¡Estas opciones ralentizarán tu conexión!</string>
@@ -223,11 +224,18 @@
<string name="tor_status">Estado de Tor</string>
<string name="snowflake_status">Estado Snowflake</string>
<string name="snowflake_started">cliente Snowflake iniciado</string>
+ <string name="snowflake_negotiating_rendezvous_http">Negociando Snowflake proxy rendezvous (http)</string>
+ <string name="snowflake_negotiating_rendezvous_amp_cache">Negociando Snowflake proxy rendezvous (amp cache)</string>
<string name="snowflake_socks_error">error Snowflake SOCKS</string>
+ <string name="snowflake_broker_success">Snowflake proxy rendezvous exitoso</string>
<string name="snowflake_sending_data">Enviando datos via Snowflake</string>
<string name="title_upcoming_connection_request">Próxima Solicitud de Conexión</string>
<string name="title_upcoming_notifications_request">Solicitud de Próximas Notificaciones</string>
<string name="title_setup_success">¡Lo tiene todo listo!</string>
<string name="setup_success_description">Haga clic en el botón de abajo para conectarse</string>
<string name="permission_rejected">Solicitud de permiso rechazada.</string>
+ <string name="select_language">Seleccionar idioma</string>
+ <string name="validation_status_success">Bueno</string>
+ <string name="validation_status_failure">Malo</string>
+ <string name="scan_qr_code">Escanear código QR</string>
</resources>
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index 898a8fe2..6d43088f 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -203,6 +203,10 @@
<string name="cancel_connection">Katkaise yhteys</string>
<string name="welcome">Tervetuloa!</string>
<string name="next">Seuraava</string>
+ <string name="qr_scanner_prompt">Lue QR-koodi</string>
<string name="details">Lisätiedot</string>
<string name="tor_status">Tor-tila</string>
+ <string name="select_language">Valitse kieli</string>
+ <string name="validation_status_success">Hyvä</string>
+ <string name="scan_qr_code">Lue QR-koodi</string>
</resources>
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index b6f91078..e769bd76 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -208,8 +208,52 @@
<string name="unknown_location">Site inconnu</string>
<string name="splash_footer">Développé par LEAP</string>
<string name="welcome">Bienvenue.</string>
+ <string name="select_provider">Choisir un fournisseur</string>
+ <string name="select_provider_description">Lorsque vous utilisez un RPV, vous transférez la confiance que vous accorde votre fournisseur d\'accès à Internet à votre fournisseur RPV. Bitmask ne se connecte qu\'à des fournisseurs ayant un bilan clair en matière de protection de la vie privée et de défense des droits.</string>
+ <string name="provider_description_riseup">Riseup fournit des outils de communication en ligne pour les personnes et les groupes qui œuvrent en faveur d\'un changement social libéral. Nous voulons créer des alternatives démocratiques et pratiquer l\'autodétermination en contrôlant nos propres moyens de communication sécurisés.</string>
<string name="next">Suivant</string>
+ <string name="add_provider_description">Bitmask se connecte à des fournisseurs fiables qui ne sont pas répertoriés publiquement. Saisissez l\'adresse URL de votre fournisseur ci-dessous.</string>
+ <string name="add_provider_prompt">Saisissez ici le fournisseur de l\'URK.</string>
+ <string name="invite_code_provider_description">Bitmask vous permet de vous connecter aux fournisseurs à l\'aide d\'un code d\'invitation privé. </string>
+ <string name="invite_code_provider_prompt">Saisissez ici votre code d\'invitation. </string>
+ <string name="qr_scanner_prompt">Balayer un code QR</string>
+ <string name="provider_description_calyx">Calyx est un organisme d\'éducation et de recherche à but non lucratif qui se consacre à l\'étude, au test, au développement et à la mise en œuvre de technologies et d\'outils de protection de la vie privée afin de promouvoir la liberté d\'expression, la liberté de parole, l\'engagement civique et le droit à la vie privée sur l\'internet et dans l\'industrie des communications mobiles.</string>
+ <string name="title_circumvention_setup">Avez-vous besoin de contourner la censure ?</string>
+ <string name="circumvention_setup_description">Si vous vivez dans un pays où l\'internet est censuré, vous pouvez recourir à nos options de contournement de la censure pour accéder à tous les services web. Ces solutions ralentiront votre connexion !</string>
+ <string name="circumvention_setup_hint">%sessaiera automatiquement de vous connecter à Internet au moyen de diverses technologies de contournement. Vous pouvez régler cela dans les paramètres avancés.</string>
+ <string name="use_standard_vpn">Utilisez %s normal</string>
+ <string name="use_circumvention_tech">Utilisez des techniques de contournement (plus lentes)</string>
+ <string name="description_configure_provider">Pour se connecter à votre fournisseur d\'accès, %1$s récupère toutes les informations de configuration requises. Cela ne se produit que lors de la première installation.</string>
+ <string name="description_configure_provider_circumvention">%1$stente de collecter auprès du fournisseur toutes les données de configuration requises. Cela est uniquement réalisé lors de la première installation. Vous avez opté d\'utiliser une technologie de contournement, ce qui peut prendre un certain temps.</string>
<string name="details">Détails</string>
<string name="tor_status">État de Tor</string>
+ <string name="snowflake_status">État de Snowflake</string>
+ <string name="snowflake_started">L\'appli client Snowflake a démarré</string>
+ <string name="snowflake_socks_error">Erreur Snowflake SOCKS</string>
+ <string name="snowflake_sending_data">Envoi de données via Snowflake</string>
+ <string name="title_upcoming_connection_request">Demande de connexion à venir</string>
+ <string name="title_upcoming_notifications_request">Demande de notifications à venir</string>
<string name="title_setup_success">Tout est prêt !</string>
+ <string name="setup_success_description">Cliquez sur le bouton ci-dessous pour vous connecter</string>
+ <string name="permission_rejected">Demande d’autorisation refusée.</string>
+ <string name="login_not_supported">La version actuelle de l\'application ne supporte pas les connexions, ce qui vous oblige à mettre à jour votre certification VPN pour ce fournisseur.</string>
+ <string name="select_language">Sélectionner une langue</string>
+ <string name="syntax_check">Vérification de la syntaxe:</string>
+ <string name="validation_status_success">Bon</string>
+ <string name="validation_status_failure">Mauvaise</string>
+ <string name="enter_invite_code">Saisir le code d\'invitation</string>
+ <string name="scan_qr_code">Balayer un code QR</string>
+ <string name="invalid_code">Le code est invalide</string>
+ <string name="automatic_bridge">Automatique (recommandé)</string>
+ <string name="automatic_bridge_description">La connexion sera tentée au moyen des meilleurs ponts et protocoles disponibles.</string>
+ <string name="manual_bridge">Configuration manuelle</string>
+ <string name="manual_bridge_description">Sélectionnez des ponts privés et des protocoles spécifiques</string>
+ <string name="censorship_circumvention_description">La configuration manuelle nécessite des connaissances techniques. Agissez avec prudence.</string>
+ <string name="discovery">Découverte</string>
+ <string name="discovery_description">Les censeurs peuvent empêcher la découverte d\'informations critiques sur la configuration de votre fournisseur. Choisissez une option de contournement pour éviter les blocages.</string>
+ <string name="automatically_select">Sélection automatique</string>
+ <string name="tunnelling">Tunnellisation</string>
+ <string name="tunnelling_description">Les censeurs peuvent bloquer l\'accès à l\'internet ouvert. Choisissez une option de contournement pour éviter les blocages.</string>
+ <string name="port_hopping">Saut de port</string>
+ <string name="port_hopping_description">Les censeurs utilisent l\'analyse du trafic pour bloquer l\'accès à l\'internet ouvert. Le saut de port peut leur compliquer la tâche. </string>
</resources>
diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml
index 562c5b8b..31f1bc58 100644
--- a/app/src/main/res/values-he/strings.xml
+++ b/app/src/main/res/values-he/strings.xml
@@ -205,6 +205,9 @@ https://0xacab.org/leap/bitmask_android/issues</string>
<string name="cancel_connection">התנתק</string>
<string name="welcome">ברוך הבא!</string>
<string name="next">הבא</string>
+ <string name="qr_scanner_prompt">סרוק קוד QR</string>
<string name="details">פרטים</string>
<string name="tor_status">מעמד Tor</string>
+ <string name="select_language">בחר שפה</string>
+ <string name="scan_qr_code">סרוק קוד QR</string>
</resources>
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index dae89cfa..3a6a907d 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -190,6 +190,10 @@
<string name="cancel_connection">Kapcsolat bontása</string>
<string name="welcome">Köszöntjük!</string>
<string name="next">Következő</string>
+ <string name="qr_scanner_prompt">QR kód beolvasása</string>
<string name="details">Részletek</string>
<string name="tor_status">Tor állapot</string>
+ <string name="select_language">Nyelv választása</string>
+ <string name="validation_status_success">Jó</string>
+ <string name="scan_qr_code">QR kód beolvasása</string>
</resources>
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index d565e7c7..d845bae1 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="retry">再試行</string>
- <string name="repository_url_text">ソースコードは https://0xacab.org/leap/bitmask_android で利用できます</string>
+ <string name="repository_url_text">ソースコードは https://0xacab.org/leap/bitmask_android で入手可能です</string>
<string name="leap_tracker">問題トラッカーは https://0xacab.org/leap/bitmask_android/issues で利用できます</string>
<string name="translation_project_text">翻訳を歓迎、感謝します。 Transifex プロジェクトをご覧ください https://www.transifex.com/projects/p/bitmask/</string>
<string name="switch_provider_menu_option">プロバイダーを切り替え</string>
@@ -40,7 +40,7 @@
<string name="error_client_http_user_message">再度実行してください: クライアントHTTPエラー</string>
<string name="error_io_exception_user_message">再度実行してください: I/Oエラー</string>
<string name="error_json_exception_user_message">再度実行してください: サーバーからの応答が正しくありません</string>
- <string name="error_no_such_algorithm_exception_user_message">暗号化アルゴリズムが見つかりません。Androidをアップデートしてください!</string>
+ <string name="error_no_such_algorithm_exception_user_message">暗号化アルゴリズムが見つかりません。Androidをアップグレードしてください!</string>
<string name="signup_or_login_button">サインアップ/ログイン</string>
<string name="login_button">ログイン</string>
<string name="login_to_profile">プロファイルにログイン</string>
@@ -54,7 +54,7 @@
<string name="setup_error_text">選択されたプロバイダーで%sを構成中にエラーが発生しました。\n\nもう一度構成するか、終了して次回の起動時にプロバイダーを構成することができます。</string>
<string name="setup_error_text_custom">%sを構成中にエラーが発生しました。\n\nもう一度構成するか、終了することができます。</string>
<string name="server_unreachable_message">サーバーに到達できません。再度実行してください。</string>
- <string name="error.security.pinnedcertificate">セキュリティエラー。アプリをアップデートするか別のプロバイダーを選択してください。</string>
+ <string name="error.security.pinnedcertificate">セキュリティエラー。アプリをアップグレードするか別のプロバイダーを選択してください。</string>
<string name="malformed_url">%sのプロバイダーではないようです。</string>
<string name="certificate_error">これは、信頼できる%sプロバイダーではありません。</string>
<string name="service_is_down_error">サービスがダウンしています。</string>
@@ -70,8 +70,8 @@
<string name="eip_status_connecting">VPNに接続中</string>
<string name="eip_status_unsecured">保護されてない接続</string>
<string name="eip_status_secured">保護された接続</string>
- <string name="eip_cancel_connect_title">接続をキャンセルしますか?</string>
- <string name="eip_cancel_connect_text">試行中の接続があります。それをキャンセルしますか?</string>
+ <string name="eip_cancel_connect_title">接続を中止しますか?</string>
+ <string name="eip_cancel_connect_text">試行中の接続があります。中止しますか?</string>
<string name="eip.warning.browser_inconsistency">VPN接続をオフにしますか? VPNをオフにすると、お使いのインターネットプロバイダーやローカルネットワークに個人情報を漏洩することがあります。</string>
<string name="eip_state_not_connected">稼働していません! 接続は安全ではありません!</string>
<string name="eip_state_connected">接続は安全です</string>
@@ -93,20 +93,20 @@
<string name="action_settings">設定</string>
<string name="void_vpn_establish">%sはすべての送信インターネット通信をブロックします</string>
<string name="void_vpn_error_establish">すべてのインターネット通信のブロックに失敗しました。</string>
- <string name="void_vpn_stopped">すべての送信インターネット通信のブロックを停止しました。</string>
- <string name="void_vpn_title">通信のブロック</string>
+ <string name="void_vpn_stopped">すべての送信インターネット通信の遮断を停止しました。</string>
+ <string name="void_vpn_title">通信を遮断</string>
<string name="update_provider_details">プロバイダーの詳細を更新</string>
<string name="update_certificate">証明書を更新</string>
<string name="warning_eip_json_corrupted">プロバイダー設定の更新に失敗しました。</string>
<string name="eip_json_corrupted_user_message">プロバイダー設定の更新に失敗しました。もう一度ログインし直してください。</string>
<string name="warning_client_parsing_error_gateways">プロバイダーゲートウェイを認識できませんでした。正しく設定されていない可能性があります。</string>
- <string name="warning_corrupted_provider_details">保存されたプロバイダーの詳細が壊れています。 %sを更新する (推奨) か、商用CA証明書を使用してプロバイダーの詳細を更新することができます。</string>
- <string name="warning_corrupted_provider_cert">保存されたプロバイダーの証明書が無効です。 商用CA証明書を使用して%sを更新する (推奨) か、プロバイダーの証明書を更新することができます。</string>
- <string name="warning_expired_provider_cert">保存されたプロバイダーの証明書が有効期限切れです。 商用CA証明書を使用して%sを更新する (推奨) か、プロバイダー証明書を更新することができます。</string>
+ <string name="warning_corrupted_provider_details">保存されたプロバイダーの詳細が壊れています。 %sを更新する(推奨)か、商用CA証明書を使用してプロバイダーの詳細を更新することができます。</string>
+ <string name="warning_corrupted_provider_cert">保存されたプロバイダーの証明書が無効です。 商用CA証明書を使用して%sを更新する(推奨)か、プロバイダーの証明書を更新することができます。</string>
+ <string name="warning_expired_provider_cert">保存されたプロバイダーの証明書が有効期限切れです。 商用CA証明書を使用して%sを更新する(推奨)か、プロバイダー証明書を更新することができます。</string>
<string name="downloading_vpn_certificate_failed">VPN証明書のダウンロードに失敗しました。 もう一度やり直すか、別のプロバイダーを選択してください。</string>
- <string name="vpn_certificate_is_invalid">VPN証明書が不正です。 新しくダウンロードし直してください。</string>
+ <string name="vpn_certificate_is_invalid">VPN証明書を更新する時間です。接続を安全にするために、新しい証明書をダウンロードしてください。これは、通常の繰り返し行われる処理です。</string>
<string name="vpn_certificate_user_message">VPN証明書が不正です。 ログインして新しくダウンロードし直してください。</string>
- <string name="save_battery">バッテリー節約</string>
+ <string name="save_battery">電池節約</string>
<string name="subtitle_save_battery">VPNホットスポットがオンの間無効にします</string>
<string name="save_battery_message">お使いの携帯電話を使用していない時に、バックグラウンドのデータ接続は休止状態になります。</string>
<string name="always_on_vpn">常時VPN接続</string>
@@ -132,13 +132,13 @@
<string name="donate_button_donate">寄付</string>
<string name="obfuscated_connection">難読化された接続を使用します。</string>
<string name="obfuscated_connection_try">難読化された接続を試行します。</string>
- <string name="nav_drawer_obfuscated_connection">ブリッジを使う</string>
+ <string name="nav_drawer_obfuscated_connection">ブリッジを使用</string>
<string name="nav_drawer_subtitle_obfuscated_connection">VPNフィルタリングを迂回</string>
<string name="warning_exclude_apps_message">注意してVPNからアプリを除外してください。これにより、身元が明らかになり、セキュリティが侵害される可能性があります。</string>
<plurals name="subtitle_exclude_apps">
<item quantity="other">%d の保護されていないアプリ</item>
</plurals>
- <string name="warning_no_more_gateways_use_pt">%sは接続できませんでした。 VPN接続がブロックされている可能性があります。難読化された接続を使用して接続を試みますか?</string>
+ <string name="warning_no_more_gateways_use_pt">%sは接続できませんでした。 VPN接続が遮断されている可能性があります。難読化された接続を使用して接続を試みますか?</string>
<string name="warning_no_more_gateways_no_pt">%s は接続できませんでした。 再試行しますか?</string>
<string name="warning_no_more_gateways_use_ovpn">%s は難読化されたVPN接続を使用して接続できませんでした。標準VPNを使用して接続を試みますか?</string>
<string name="warning_no_more_gateways_manual_gw_selection">%1$sは%2$sへ接続できませんでした。最適な場所へ自動的な接続を試みますか?</string>
@@ -146,7 +146,7 @@
<string name="warning_option_try_pt">難読化された接続を試みる</string>
<string name="warning_option_try_ovpn">標準接続を試みる</string>
<string name="vpn_error_establish">AndroidはVPNサービスを確立できませんでした。</string>
- <string name="root_permission_error">root権限がないと %s がVPNホットスポットやIPv6ファイアウォールなどの機能を実行できません。</string>
+ <string name="root_permission_error">root権限なしに %s がVPNホットスポットやIPv6ファイアウォールなどの機能を実行できません。</string>
<string name="qs_enable_vpn">%s 開始</string>
<string name="version_update_found">ここをタップしてダウンロードを開始する</string>
<string name="version_update_title">新しいバージョン %s が見つかりました</string>
@@ -166,19 +166,25 @@
<string name="tor_starting">検閲を回避するためにブリッジを起動中…</string>
<string name="tor_stopping">ブリッジを停止</string>
<string name="tor_started">検閲を回避するためにブリッジを使用</string>
- <string name="log_conn_done_pt">pluggable transportへ接続しました</string>
- <string name="log_conn_pt">pluggable transportへ接続中です</string>
- <string name="log_conn_done">中継へ接続しました</string>
+ <string name="log_conn_done_pt">pluggable transportに接続しました</string>
+ <string name="log_conn_pt">pluggable transportに接続中です</string>
+ <string name="log_conn_done">中継に接続しました</string>
<string name="log_handshake">中継と接続をネゴシエート中です</string>
<string name="log_handshake_done">中継と接続をネゴシエートしました</string>
<string name="log_onehop_create">暗号化されたディレクトリとの接続を確立中</string>
+ <string name="log_requesting_status">ネットワーク状態の合意を要求中</string>
+ <string name="log_loading_status">ネットワーク状態の合意を読み込み中</string>
<string name="log_loading_keys">認証局の署名を読込中</string>
<string name="log_requesting_descriptors">中継の記述子を尋ねています</string>
<string name="log_loading_descriptors">中継の記述子を読み込み中です</string>
<string name="log_enough_dirinfo">回路を構築する必要なディレクトリ情報を読み込みました</string>
<string name="log_ap_handshake_done">中継で回路を構築するネゴシエーションが終了しました</string>
- <string name="log_circuit_create">Tor サーキットを設置しています</string>
+ <string name="log_circuit_create">Tor 回路を確立しています</string>
<string name="log_done">実行中</string>
+ <string name="channel_name_tor_service">%sブリッジサービス</string>
+ <string name="channel_description_tor_service">%sの構成中にブリッジの使用について伝えます。</string>
+ <string name="error_tor_timeout">ブリッジの開始に失敗しました。再試行、または%sを構成してobfuscateではない安全な接続で続行しますか?</string>
+ <string name="retry_unobfuscated">obfuscateなしに再試行</string>
<string name="hide">隠す</string>
<string name="error_network_connection">%sはインターネット接続がありません。WiFiとセルラーデータの設定を確認してください。</string>
<string name="censorship_circumvention">検閲を回避</string>
@@ -193,26 +199,58 @@
<string name="eip_state_insecure">接続は安全ではありません</string>
<string name="connection_not_connected">あなたのインターネットプロバイダーまたはローカルネットワークに情報が漏洩するかもしれません。</string>
<string name="eip_state_no_network">インターネットに接続できない状態です。インターネット接続が戻れば、自動的に接続します</string>
- <string name="eip_state_blocking">%1$sはすべてのインターネット転送をプロックしています。</string>
+ <string name="eip_state_blocking">%1$sはすべてのインターネット転送を遮断しています。</string>
<string name="disabled_while_udp_on">UDPがオンの間は無効化されます。</string>
- <string name="advanced_settings">詳細な設定</string>
+ <string name="advanced_settings">詳細設定</string>
<string name="cancel_connection">切断</string>
<string name="unknown_location">未知の場所</string>
<string name="splash_footer">LEAPによって開発されました</string>
<string name="welcome">ようこそ!</string>
<string name="select_provider">プロバイダーを選択</string>
+ <string name="select_provider_description">VPNを使用するとインターネットサービスプロバイダーからVPNプロバイダーへ、信頼されなければならない組織が移ります。Bitmaskは、プライバシーの保護と擁護の明瞭な歴史があるプロバイダーに限って接続します。</string>
+ <string name="provider_description_riseup">プロバイダーRiseupは、解放的社会変革に粘り強く取り組む事業団体です。我々は新方式の民主主義を創り、通信の安全を意図して自己制御による自己決定を実践します。</string>
<string name="next">次へ</string>
+ <string name="add_provider_description">Bitmaskは、公開一覧に記載されていないプロバイダーに接続できます。追加するプロバイダーは、あなたが知っていて、信頼しているものであることを確かめてください。</string>
+ <string name="add_provider_prompt">ここに、プロバイダーのURLを入力してください。</string>
+ <string name="invite_code_provider_description">Bitmaskは非公開の招待文字列を使用してプロバイダーに接続できます。</string>
+ <string name="invite_code_provider_prompt">ここに、信用する招待文字列を入力してください。</string>
+ <string name="qr_scanner_prompt">QR コードをスキャン</string>
+ <string name="provider_description_calyx">Calyxは、プライバシー技術を検討、試験、開発及び実装し、言論の自由、表現の自由、市民の参画及びインターネットと携帯電話通信事業でプライバシー権を助長する道具の製作に取り組む、教育と研究の非営利団体です。</string>
+ <string name="title_circumvention_setup">検閲の回避策が必要ですか?</string>
+ <string name="circumvention_setup_description">インターネットが検閲された地域にお住まいであれば、全てのインターネットサービスを利用するために検閲回避策を使用できます。これらの策を使用すると、接続が低速になります。</string>
+ <string name="circumvention_setup_hint">%sは自動的に様々な検閲回避技術を使用して、インターネットへ接続を試みます。詳細設定内でこれを細かく調整できます。</string>
+ <string name="use_standard_vpn">標準の%sを使用</string>
+ <string name="use_circumvention_tech">検閲回避技術を使用(低速)</string>
+ <string name="description_configure_provider">プロバイダーに接続するために、%1$sは必要な全ての設定情報を取得します。これは最初のセットアップ時にのみ発生します。</string>
+ <string name="description_configure_provider_circumvention">%1$sはプロバイダーから必要な設定データを全て収集しようとしています。これは最初のセットアップ時にのみ発生します。検閲迂回技術の使用を選択すると、時間がかかる場合があります。</string>
<string name="details">詳細</string>
<string name="tor_status">Tor の状態</string>
<string name="snowflake_status">Snowflake の状態</string>
<string name="snowflake_started">Snowflake クライアントを開始しました</string>
+ <string name="snowflake_negotiating_rendezvous_http">Snowflakeプロキシ接触接続情報交換中(http)</string>
+ <string name="snowflake_negotiating_rendezvous_amp_cache">Snowflakeプロキシ接触接続情報交換中(amp cache)</string>
+ <string name="snowflake_socks_error">Snowflake SOCKSエラー</string>
+ <string name="snowflake_broker_success">Snowflakeプロキシ接触成功</string>
<string name="snowflake_sending_data">Snowflake 経由でデータを送信中</string>
<string name="title_upcoming_connection_request">今度は接続要求だ</string>
- <string name="upcoming_connection_request_description">次の小枠でAndroidがVPNプロバイダーを信用するか、必須の確認を行います。Bitmaskは、VPN用に最良の実践がある厳格なプライバシーを遵守し、利用者のデータと識別情報の保護の検証可能な歴史があるプロバイダーに限って提携します。</string>
<string name="title_upcoming_notifications_request">今度は通知要求だ</string>
- <string name="upcoming_notifications_request_description">次の小枠でAndroidが通知を許可するかをお尋ねします。これは、安定的なバックグラウンド接続を確実にし、Androidの通知センター内からデータ使用量を確認可能にします。</string>
<string name="title_setup_success">全て設定しました!</string>
<string name="setup_success_description">接続するには、下のボタンをクリック</string>
<string name="permission_rejected">権限の要求が拒否されました。</string>
<string name="login_not_supported">現在のアプリのバージョンは、このプロバイダー用のVPN証明書を更新するのに必要なログインに対応しません。</string>
+ <string name="select_language">言語の選択</string>
+ <string name="validation_status_success">良</string>
+ <string name="enter_invite_code">招待文字列を入力</string>
+ <string name="scan_qr_code">QR コードをスキャン</string>
+ <string name="invalid_code">無効なコード</string>
+ <string name="automatic_bridge">自動(推奨)</string>
+ <string name="automatic_bridge_description">接続は、利用可能な最良のブリッジとプロトコルの使用を試行します。</string>
+ <string name="manual_bridge">手動設定</string>
+ <string name="manual_bridge_description">非公開のブリッジと特定のプロトコルを選択します</string>
+ <string name="censorship_circumvention_description">手動設定には、技術の理解が必要です。慎重に続行してください。</string>
+ <string name="discovery_description">検閲官は、プロバイダーからの極めて重要な設定情報の発見を遮断できます。遮断を回避するために、検閲回避法を選択してください。</string>
+ <string name="tunnelling">トンネリング</string>
+ <string name="tunnelling_description">検閲官は、開放的インターネットへのアクセスを遮断できます。遮断を回避するために、検閲回避法を選択してください。</string>
+ <string name="port_hopping">ポート探索(ポートホッピング)</string>
+ <string name="port_hopping_description">検閲官は、開放的インターネットへのアクセスを遮断するために、転送の分析を使用します。ポート探索(ポートホッピング)は転送の分析を難しくすることができます。</string>
</resources>
diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml
index 6314ca8f..e0220d4d 100644
--- a/app/src/main/res/values-lt/strings.xml
+++ b/app/src/main/res/values-lt/strings.xml
@@ -104,7 +104,7 @@
<string name="warning_corrupted_provider_cert">Laikomas teikėjo liudijimas negalioja. Jūs galite arba atnaujinti %s (rekomenduojama), arba atnaujinti teikėjo liudijimą, naudodami komercinį LĮ liudijimą.</string>
<string name="warning_expired_provider_cert">Laikomas teikėjo liudijimas nebegalioja. Jūs galite arba atnaujinti %s (rekomenduojama), arba atnaujinti teikėjo liudijimą, naudodami komercinį LĮ liudijimą.</string>
<string name="downloading_vpn_certificate_failed">Nepavyko atsisiųsti VPN liudijimo. Bandykite dar kartą arba pasirinkite kitą teikėją.</string>
- <string name="vpn_certificate_is_invalid">VPN liudijimas negalioja. Pabandykite atsisiųsti naują.</string>
+ <string name="vpn_certificate_is_invalid">Metas atnaujinti jūsų VPN liudijimą. Atsisiųskite naują liudijimą, kad išlaikytumėte savo ryšį saugų. Tai yra įprastas atnaujinimas.</string>
<string name="vpn_certificate_user_message">VPN liudijimas negalioja. Prisijunkite norėdami atsisiųsti naują.</string>
<string name="save_battery">Taupyti akumuliatoriaus energiją</string>
<string name="subtitle_save_battery">Išjungta, kol įjungtas VPN viešosios interneto prieigos taškas</string>
@@ -214,6 +214,10 @@
<string name="provider_description_riseup">„Riseup“ teikia internetinio susisiekimo įrankius žmonėms ir grupėms, siekiantiems išvaduojamųjų socialinių pokyčių. Mes esame projektas, kurio tikslas – kurti demokratines alternatyvas ir praktikuoti apsisprendimo teisę, valdant mūsų asmenines saugias komunikacijos priemones.</string>
<string name="next">Kitas</string>
<string name="add_provider_description">Bitmask jungiasi prie patikimų teikėjų, kurie nėra viešai išvardyti. Žemiau įveskite savo teikėjo URL adresą.</string>
+ <string name="add_provider_prompt">Čia įrašykite teikėjo URL adresą.</string>
+ <string name="invite_code_provider_description">Bitmask jums leidžia prisijungti prie teikėjų naudojant privatų pakvietimo kodą. </string>
+ <string name="invite_code_provider_prompt">Čia įveskite savo patikimą pakvietimo kodą.</string>
+ <string name="qr_scanner_prompt">Skenuoti QR kodą</string>
<string name="provider_description_calyx">„Calyx“ yra ne pelno siekianti švietimo ir tyrinėjimų organizacija, pasišventusi tyrinėti, testuoti, plėtoti ir įgyvendinti privatumo technologijas bei įrankius, skatinant žodžio laisvę, laisvą raišką, piliečių įsitraukimą ir teises į privatumą internete bei mobiliųjų komunikacijų pramonėje.</string>
<string name="title_circumvention_setup">Ar jums reikia cenzūros apėjimo?</string>
<string name="circumvention_setup_description">Jei gyvenate šalyje, kurioje internetas yra cenzūruojamas, galite naudoti mūsų cenzūros apėjimo parinktis, kad pasiektumėte visas interneto paslaugas. Šios parinktys sulėtins jūsų interneto ryšio greitį!</string>
@@ -232,11 +236,28 @@
<string name="snowflake_broker_success">„Snowflake“ įgaliotojo serverio pasimatymas sėkmingas</string>
<string name="snowflake_sending_data">Siunčiami duomenys per „Snowflake“</string>
<string name="title_upcoming_connection_request">Būsima ryšio užklausa</string>
- <string name="upcoming_connection_request_description">Kitame langelyje „Android“ primins jums apie tai, kad yra būtina pasitikėti savo VPN teikėju. Bitmask bendradarbiauja tik su partneriais, kurie tvirtai laikosi geriausių ir griežtų VPN privatumo įpročių ir turi įrodomą naudotojų duomenų ir tapatybių apsaugos istoriją.</string>
<string name="title_upcoming_notifications_request">Būsima pranešimų užklausa</string>
- <string name="upcoming_notifications_request_description">Kitame langelyje „Android“ paklaus jūsų, ar norite leisti pranešimus. Tai užtikrins stabilų foninį ryšį ir leis jums „Android“ pranešimų centre matyti duomenų naudojimą.</string>
<string name="title_setup_success">Viskas paruošta!</string>
<string name="setup_success_description">Norėdami prisijungti, spustelėkite žemiau esantį mygtuką</string>
<string name="permission_rejected">Leidimo užklausa atmesta.</string>
<string name="login_not_supported">Dabartinė programėlės versija nepalaiko prisijungimų, kurių reikia, kad atnaujintumėte savo VPN liudijimą šiam teikėjui.</string>
+ <string name="select_language">Pasirinkti kalbą</string>
+ <string name="syntax_check">Sintaksės tikrinimas:</string>
+ <string name="validation_status_success">Gerai</string>
+ <string name="validation_status_failure">Blogai</string>
+ <string name="enter_invite_code">Įveskite pakvietimo kodą</string>
+ <string name="scan_qr_code">Skenuoti QR kodą</string>
+ <string name="invalid_code">Neteisingas kodas</string>
+ <string name="automatic_bridge">Automatiškai (rekomenduojama)</string>
+ <string name="automatic_bridge_description">Bus bandoma užmegzti ryšį naudojant geriausius prieinamus tinklų tiltus bei protokolus.</string>
+ <string name="manual_bridge">Rankinė konfigūracija</string>
+ <string name="manual_bridge_description">Pasirinkti privačius tinklų tiltus ir tam tikrus protokolus</string>
+ <string name="censorship_circumvention_description">Rankinė konfigūracija reikalauja techninių žinių. Tęskite apdairiai.</string>
+ <string name="discovery">Atradimas</string>
+ <string name="discovery_description">Cenzoriai gali blokuoti informacijos apie kritinę konfigūraciją atradimą iš jūsų teikėjo. Pasirinkite apėjimo parinktį, skirtą apeiti blokuotes. </string>
+ <string name="automatically_select">Pasirinkti automatiškai</string>
+ <string name="tunnelling">Tuneliavimas</string>
+ <string name="tunnelling_description">Cenzoriai gali blokuoti prieigą prie atvirojo interneto. Pasirinkite apėjimo parinktį, skirtą apeiti blokuotes.</string>
+ <string name="port_hopping">Prievadų keitinėjimas</string>
+ <string name="port_hopping_description">Cenzoriai naudoja duomenų srauto analizę, kad blokuotų prieigą prie atvirojo interneto. Prievadų keitinėjimas gali jiems tai apsunkinti.</string>
</resources>
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 36a56999..46975559 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -214,18 +214,35 @@ Je kan kiezen om te herconfigureren of af te sluiten en bij de volgende start ee
<string name="provider_description_riseup">Riseup biedt online communicatiemiddelen voor mensen en groepen die werken aan sociale verandering. Wij zijn een project dat democratische alternatieven creëert en zelfbeschikking in de praktijk brengt door onze eigen veilige communicatiemiddelen te beheren.</string>
<string name="next">Volgende</string>
<string name="add_provider_description">Bitmask maakt verbinding met betrouwbare aanbieders die niet openbaar vermeld zijn. Vul hieronder de URL van jouw aanbieder in.</string>
+ <string name="qr_scanner_prompt">QR-code scannen</string>
+ <string name="provider_description_calyx">Calyx is een non-profit voor onderwijs en onderzoek die zich focust op het bestuderen, testen, ontwikkelen en implementeren van privacytechnologie en -hulpmiddelen om vrije meningsuiting, expressie, burgerparticipatie en privacyrechten op internet en in de mobiele communicatiesector te promoten.</string>
<string name="title_circumvention_setup">Moet je censuur omzeilen?</string>
<string name="circumvention_setup_description">Als je in ergens woont waar het internet gecensureerd is, kun je onze opties om censuur te ontwijken gebruiken en zo toegang te krijgen tot alle internetdiensten. Deze opties zullen je verbinding vertragen!</string>
<string name="circumvention_setup_hint">%s zal automatisch proberen te verbinden met het internet met verschillende censuur omzeilende technologieën. Dit kun je verfijnen in de geavanceerde instellingen.</string>
<string name="use_standard_vpn">Gebruik standaard %s</string>
<string name="use_circumvention_tech">Gebruik censuur omzeilende technologie (trager)</string>
+ <string name="description_configure_provider">Om te verbinden met je provider, haalt %1$s, alle configuratie details op. Dit gebeurd alleen tijdens de eerste installatie.</string>
+ <string name="description_configure_provider_circumvention">%1$s probeert alle configuratie details van de provider op te halen. Dit gebeurt alleen tijdens de eerste installatie. U heeft gekozen om censuur omzeilendefuncties te gebruiken, dus dit kan enige tijd duren.</string>
<string name="details">Details</string>
<string name="tor_status">Tor-status</string>
<string name="snowflake_status">Snowflake status</string>
<string name="snowflake_started">Snowflake client status</string>
+ <string name="snowflake_negotiating_rendezvous_http">Bezig met Snowflake proxy verbinding (http)</string>
+ <string name="snowflake_negotiating_rendezvous_amp_cache">Bezig met Snowflake proxy verbinding (amp cache)</string>
<string name="snowflake_socks_error">Snowflake SOCKS fout</string>
+ <string name="snowflake_broker_success">Snowflake proxy rendezvous succesvol</string>
<string name="snowflake_sending_data">Data versturen via Snowflake</string>
+ <string name="title_upcoming_connection_request">Nu volgt een verzoek om een verbinding te maken.</string>
+ <string name="title_upcoming_notifications_request">Nu volgt de vraag om meldingen toe te staan.</string>
<string name="title_setup_success">Je bent klaar! </string>
<string name="setup_success_description">Tik hieronder om een verbinding te maken</string>
<string name="permission_rejected">Toestemmingsverzoek afgewezen.</string>
+ <string name="login_not_supported">Je kunt in deze versie van de app niet inloggen, wat je nodig hebt om de VPN certificaten van deze provider te updaten.</string>
+ <string name="select_language">Selecteer taal</string>
+ <string name="validation_status_success">Goed</string>
+ <string name="enter_invite_code">Voer de uitnodigingscode in</string>
+ <string name="scan_qr_code">QR-code scannen</string>
+ <string name="invalid_code">Ongeldige code</string>
+ <string name="automatic_bridge">Automatisch (aanbevolen)</string>
+ <string name="automatically_select">Automatisch geselecteerd</string>
</resources>
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 993486d3..81f77a0a 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -104,7 +104,7 @@
<string name="warning_corrupted_provider_cert">O certificado armazenado do provedor é invalido. Você pode atualizar %s (recomendado) ou atualizar o certificado do provedor utilizando um certificado CA comercial. </string>
<string name="warning_expired_provider_cert">O certificado do provedor está expirado. Você pode atualizar %s (recomendado) ou atualizar o certificado do provedor utilizando um certificado CA comercial. </string>
<string name="downloading_vpn_certificate_failed">Download do certificado VPN falhou. Tente novamente ou escolha outro provedor.</string>
- <string name="vpn_certificate_is_invalid">Certificado VPN inválido. Tente baixar um novo.</string>
+ <string name="vpn_certificate_is_invalid">É hora de atualizar seu certificado VPN. Faça o download de um novo certificado para manter sua conexão segura. Esta é uma atualização de rotina.</string>
<string name="vpn_certificate_user_message">O certificado VPN é inválido. Por favor, faça login e baixe um novo.</string>
<string name="save_battery">Economizar energia</string>
<string name="subtitle_save_battery">Desabilitado enquanto o Ponto de acesso VPN estiver ligado</string>
@@ -212,7 +212,11 @@
<string name="select_provider_description">Ao usar uma VPN, você está transferindo sua confiança do seu provedor de serviços de Internet para o seu provedor de VPN. O Bitmask se conecta apenas a provedores com um histórico claro de proteção e defesa da privacidade.</string>
<string name="provider_description_riseup">Riseup fornece ferramentas de comunicação online para pessoas e grupos que trabalham em mudanças sociais libertadoras. Somos um projeto para criar alternativas democráticas e praticar a autodeterminação, controlando os nossos próprios meios de comunicação seguros.</string>
<string name="next">Próximo</string>
- <string name="add_provider_description">Bitmask se conecta a provedores confiáveis ​​que não estão listados publicamente. Insira o URL do seu provedor abaixo.</string>
+ <string name="add_provider_description">O Bitmask permite que você se conecte a provedores que não estão listados publicamente. Certifique-se de conhecer e confiar no provedor que você está adicionando.</string>
+ <string name="add_provider_prompt">Digite o URL do provedor aqui.</string>
+ <string name="invite_code_provider_description">O Bitmask permite que você se conecte a provedores usando um código de convite privado. </string>
+ <string name="invite_code_provider_prompt">Digite seu código de convite confiável aqui.</string>
+ <string name="qr_scanner_prompt">Ler QR Code</string>
<string name="provider_description_calyx">Calyx é uma organização sem fins lucrativos de educação e pesquisa dedicada a estudar, testar, desenvolver e implementar tecnologias e ferramentas de privacidade para promover a liberdade de expressão, a liberdade de expressão, o envolvimento cívico e os direitos de privacidade na Internet e na indústria de comunicações móveis.</string>
<string name="title_circumvention_setup">Você exige evasão da censura?</string>
<string name="circumvention_setup_description">Se você mora em um local onde a Internet é censurada, você pode usar nossas opções de evasão de censura para acessar todos os serviços da Internet. Essas opções irão desacelerar sua conexão!</string>
@@ -231,11 +235,34 @@
<string name="snowflake_broker_success">Encontro do proxy Snowflake bem-sucedido</string>
<string name="snowflake_sending_data">Enviando dados via Snowflake</string>
<string name="title_upcoming_connection_request">Próxima solicitação de conexão</string>
- <string name="upcoming_connection_request_description">No próximo painel o Android irá lembrá-lo de que é essencial confiar no seu provedor de VPN. A Bitmask faz parceria apenas com provedores que aderem às práticas recomendadas de privacidade estritas para VPNs e têm um histórico verificável de proteção de dados e identidades do usuário.</string>
+ <string name="title_upcoming_request">Próximas solicitações</string>
+ <string name="title_upcoming_request_summary">Nos próximos painéis, o Android pedirá sua permissão na forma de uma Solicitação de Conexão e Solicitação de Notificação.</string>
+ <string name="title_upcoming_connection_request_summary_custom">Aceitar a Solicitação de Conexão é essencial para usar a funcionalidade principal de %s.</string>
+ <string name="title_upcoming_connection_request_summary">Para a solicitação de conexão, é importante saber que a Bitmask só faz parceria com provedores parceiros confiáveis que aderem às práticas recomendadas para VPNs e têm um histórico verificável de proteção dos dados e identidades do usuário. No entanto, se você estiver se conectando manualmente a um provedor não público, certifique-se de confiar neles.</string>
+ <string name="title_upcoming_notification_request_summary">Aceitar a solicitação de notificação permite que o aplicativo seja executado em segundo plano e permite que você veja o uso de dados no centro de notificações do Android.</string>
<string name="title_upcoming_notifications_request">Próxima solicitação de notificações</string>
- <string name="upcoming_notifications_request_description">No próximo painel o Android perguntará se você deseja permitir notificações. Isso garantirá uma conexão em segundo plano estável e permitirá que você veja o uso de dados na central de notificações do Android.</string>
<string name="title_setup_success">Está tudo pronto!</string>
<string name="setup_success_description">Clique no botão abaixo para conectar</string>
<string name="permission_rejected">Solicitação de permissão rejeitada.</string>
<string name="login_not_supported">A versão atual do aplicativo não oferece suporte a logins, necessários para atualizar seu certificado VPN para este provedor.</string>
+ <string name="select_language">Selecionar idioma</string>
+ <string name="syntax_check">Verificação de sintaxe:</string>
+ <string name="validation_status_success">Bom</string>
+ <string name="validation_status_failure">Mau</string>
+ <string name="enter_invite_code">Digite o código de convite</string>
+ <string name="scan_qr_code">Ler QR Code</string>
+ <string name="invalid_code">Código inválido</string>
+ <string name="automatic_bridge">Automático (recomendado)</string>
+ <string name="automatic_bridge_description">A conexão será tentada usando as melhores pontes e protocolos disponíveis.</string>
+ <string name="manual_bridge">Configuração manual</string>
+ <string name="manual_bridge_description">Selecione pontes privadas e protocolos específicos</string>
+ <string name="censorship_circumvention_description">A configuração manual requer compreensão técnica. Prossiga com cautela.</string>
+ <string name="discovery">Descobrir</string>
+ <string name="discovery_description">Os sensores podem bloquear a descoberta de informações críticas de configuração do seu provedor. Escolha uma opção de evasão para contornar blocos.</string>
+ <string name="automatically_select">Selecione automaticamente</string>
+ <string name="invite_proxy">Código Proxy</string>
+ <string name="tunnelling">Tunneling</string>
+ <string name="tunnelling_description">Os censores podem bloquear o acesso à internet aberta. Escolha uma opção de evasão para contornar blocos.</string>
+ <string name="port_hopping">Port Hopping</string>
+ <string name="port_hopping_description">Os censores usam a análise de tráfego para bloquear o acesso à internet aberta. Port Hopping pode tornar isso mais difícil para eles.</string>
</resources>
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 43fe5b8a..9c906834 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -12,7 +12,7 @@ https://0xacab.org/leap/bitmask_android/issues</string>
<string name="routes_info">Маршруты: %s</string>
<string name="routes_info6">Маршруты IPv6: %s</string>
<string name="error_empty_username">Имя пользователя не должно быть пустым</string>
- <string name="cert_from_keystore">Получен сертификат \"%s\" из хранилища ключей</string>
+ <string name="cert_from_keystore">Получен сертификат \'%s\' из хранилища ключей</string>
<string name="provider_label">Провайдер:</string>
<string name="provider_label_none">Провайдер не настроен</string>
<string name="status_unknown">Статус неизвестен.</string>
@@ -34,7 +34,7 @@ https://0xacab.org/leap/bitmask_android/issues</string>
<string name="password_mismatch">Пароли не совпадают</string>
<string name="user_message">Сообщение пользователя</string>
<string name="about_fragment_title">О нас</string>
- <string name="exclude_apps_fragment_title">Исключение приложений</string>
+ <string name="exclude_apps_fragment_title">Исключение приложений из VPN</string>
<string name="error_srp_math_error_user_message">Повторите попытку: ошибка сервера</string>
<string name="error_bad_user_password_user_message">Неправильное имя пользователя или пароль</string>
<string name="error_not_valid_password_user_message">Длина должна быть не менее 8 символов</string>
@@ -85,7 +85,7 @@ https://0xacab.org/leap/bitmask_android/issues</string>
<string name="vpn.button.turn.off">ВЫКЛ</string>
<string name="vpn_button_turn_off_blocking">Остановить блокирование</string>
<string name="vpn_securely_routed">Ваш трафик безопасно маршрутизируется через:</string>
- <string name="vpn_securely_routed_no_internet">Подключение к интернету отсутствует. Когда оно восстановится, мы безопасно передадим ваши сообщения:</string>
+ <string name="vpn_securely_routed_no_internet">Подключение к интернету отсутствует. Когда оно восстановится, мы безопасно перенаправим ваш трафик через:</string>
<string name="log_fragment_title">Журнал</string>
<string name="vpn_fragment_title">VPN</string>
<string name="navigation_drawer_open">Открыть панель навигации</string>
@@ -100,12 +100,12 @@ https://0xacab.org/leap/bitmask_android/issues</string>
<string name="update_certificate">Обновление сертификата</string>
<string name="warning_eip_json_corrupted">Невозможно обновить конфигурацию провайдера.</string>
<string name="eip_json_corrupted_user_message">Невозможно обновить конфигурацию провайдера. Войдите, чтобы повторить попытку.</string>
- <string name="warning_client_parsing_error_gateways">Невозможно распознать шлюзы поставщика. Они могут быть настроены неправильно.</string>
- <string name="warning_corrupted_provider_details">Сохранённые сведения о провайдере повреждены. Можно либо обновить %s (рекомендуется), либо обновить сведения о поставщике с помощью коммерческого сертификата ЦС.</string>
+ <string name="warning_client_parsing_error_gateways">Невозможно распознать шлюзы провайдера. Они могут быть настроены неправильно.</string>
+ <string name="warning_corrupted_provider_details">Сохранённые сведения о провайдере повреждены. Можно либо обновить %s (рекомендуется), либо обновить сведения о провайдере с помощью коммерческого сертификата ЦС.</string>
<string name="warning_corrupted_provider_cert">Сохранённый сертификат провайдера недействителен. Вы можете либо обновить %s (рекомендуется), либо обновить сертификат провайдера используя коммерческий сертификат центра сертификации.</string>
- <string name="warning_expired_provider_cert">Срок действия сохранённого сертификата провайдера истёк. Можно либо обновить %s (рекомендуется), либо обновить сертификат поставщика с помощью коммерческого сертификата ЦС.</string>
+ <string name="warning_expired_provider_cert">Срок действия сохранённого сертификата провайдера истёк. Можно либо обновить %s (рекомендуется), либо обновить сертификат провайдера с помощью коммерческого сертификата ЦС.</string>
<string name="downloading_vpn_certificate_failed">Загрузка сертификата VPN не выполнена. Попробуйте ещё раз или выберите другого провайдера.</string>
- <string name="vpn_certificate_is_invalid">Сертификат VPN недействителен. Попытайтесь загрузить новый.</string>
+ <string name="vpn_certificate_is_invalid">Пришло время обновить сертификат VPN. Загрузите новый сертификат, чтобы обеспечить безопасность соединения. Это рутинное обновление.</string>
<string name="vpn_certificate_user_message">Сертификат VPN недействителен. Войдите для загрузки нового.</string>
<string name="save_battery">Экономия батареи</string>
<string name="subtitle_save_battery">Отключено при использовании точки доступа VPN</string>
@@ -125,7 +125,7 @@ https://0xacab.org/leap/bitmask_android/issues</string>
<string name="tethering_bluetooth">Bluetooth-модем</string>
<string name="do_not_show_again">Не показывать снова</string>
<string name="always_on_vpn_user_message">Для включения постоянной VPN в настройках Android, нажмите значок конфигурации [img src] и переведите его во включённое состояние.</string>
- <string name="always_on_blocking_vpn_user_message">Чтобы оптимально защитить конфиденциальность, вы также должны активировать функцию \"Блокировать подключения без VPN\".</string>
+ <string name="always_on_blocking_vpn_user_message">Чтобы оптимально защитить конфиденциальность, вы также должны активировать функцию \'Блокировать подключения без VPN\'.</string>
<string name="donate_title">Пожертвование</string>
<string name="donate_default_message">Пожалуйста, сделайте пожертвование сегодня, если вы цените безопасное общение, простое как для конечного пользователя, так и для поставщика услуг.</string>
<string name="donate_message">LEAP зависит от пожертвований и грантов. Пожалуйста, сделайте пожертвование сегодня, если вы цените безопасное общение, простое как для конечного пользователя, так и для поставщика услуг.</string>
@@ -144,13 +144,13 @@ https://0xacab.org/leap/bitmask_android/issues</string>
</plurals>
<string name="warning_no_more_gateways_use_pt">%s не подключается. Возможно, что VPN-подключения блокируются. Попробовать подключиться с помощью маскировки подключения?</string>
<string name="warning_no_more_gateways_no_pt">%s не удалось подключиться. Повторить попытку?</string>
- <string name="warning_no_more_gateways_use_ovpn">%s не подключается с помощью маскировки VPN-подключения. Попробовать подключиться с помощью стандартного VPN?</string>
+ <string name="warning_no_more_gateways_use_ovpn">%s не подключается с помощью маскированного VPN-подключения. Попробовать подключиться с помощью стандартного VPN?</string>
<string name="warning_no_more_gateways_manual_gw_selection">%1$s не может подключиться к %2$s. Использовать автоматический выбор лучшего расположения?</string>
<string name="warning_option_try_best">Попробовать лучшее расположение</string>
<string name="warning_option_try_pt">Попробовать маскировку подключения</string>
<string name="warning_option_try_ovpn">Попробовать стандартное подключение</string>
<string name="vpn_error_establish">Android не удалось установить службу VPN.</string>
- <string name="root_permission_error">%s не может использовать такие функции, как точка доступа VPN или блокировка IPv6, без root-прав.</string>
+ <string name="root_permission_error">%s не может использовать такие функции, как точка доступа VPN или брандмауэр IPv6, без root-прав.</string>
<string name="qs_enable_vpn">Запустить %s</string>
<string name="version_update_found">Нажмите здесь, чтобы начать загрузку.</string>
<string name="version_update_title">Найдена новая версия %s.</string>
@@ -205,7 +205,7 @@ https://0xacab.org/leap/bitmask_android/issues</string>
<string name="eip_state_no_network">Отсутствует рабочее подключение к интернету. Как только оно появится, вы будете автоматически подключены к</string>
<string name="eip_state_blocking">%1$s блокирует весь интернет-трафик.</string>
<string name="disabled_while_udp_on">Отключено при использовании UDP.</string>
- <string name="advanced_settings">Дополнительно</string>
+ <string name="advanced_settings">Расширенные настройки</string>
<string name="cancel_connection">Отключить</string>
<string name="unknown_location">Неизвестное местоположение</string>
<string name="splash_footer">Разработано LEAP</string>
@@ -214,15 +214,19 @@ https://0xacab.org/leap/bitmask_android/issues</string>
<string name="select_provider_description">При использовании VPN вы передаёте своё доверие от интернет-провайдера к VPN-провайдеру. Bitmask подключается только к тем провайдерам, которые имеют чистую историю защиты и отстаивания конфиденциальности.</string>
<string name="provider_description_riseup">Riseup предоставляет инструменты онлайн-коммуникации для людей и групп, работающих над освободительными социальными изменениями. Мы — проект, направленный на создание демократических альтернатив и самоопределения путём контроля за безопасностью собственных средств связи.</string>
<string name="next">Далее</string>
- <string name="add_provider_description">Bitmask подключается к надёжным провайдерам, которых нет в открытом доступе. Введите ниже URL вашего провайдера.</string>
+ <string name="add_provider_description">Bitmask позволяет подключаться к провайдерам, которые не публичны. Убедитесь, что вы знаете и доверяете провайдеру, которого добавляете.</string>
+ <string name="add_provider_prompt">Введите здесь URL-адрес провайдера.</string>
+ <string name="invite_code_provider_description">Bitmask позволяет использовать провайдеров с помощью приватного кода приглашения. </string>
+ <string name="invite_code_provider_prompt">Введите ваш надежный код приглашения здесь.</string>
+ <string name="qr_scanner_prompt">Сканировать QR-код</string>
<string name="provider_description_calyx">Calyx — некоммерческая образовательная и исследовательская организация, занимающаяся изучением, тестированием, разработкой и внедрением технологий и инструментов обеспечения конфиденциальности для продвижения свободы слова, свободы самовыражения, гражданской активности и прав на неприкосновенность частной жизни в интернете и в индустрии мобильной связи.</string>
<string name="title_circumvention_setup">Вам требуется обход цензуры?</string>
- <string name="circumvention_setup_description">Если вы живёте там, где интернет цензурируется, то можете использовать наши средства обхода цензуры для доступа ко всем услугам интернета. Это замедлят подключение!</string>
+ <string name="circumvention_setup_description">Если вы живёте там, где интернет цензурируется, то можете использовать наши средства обхода цензуры для доступа ко всем услугам интернета. Это замедлит подключение!</string>
<string name="circumvention_setup_hint">%s автоматически попытается подключить вас к интернету, используя различные технологии обхода. Параметры можно изменить в настройках.</string>
<string name="use_standard_vpn">Использовать стандарт %s</string>
<string name="use_circumvention_tech">Использовать технологию обхода (медленнее)</string>
<string name="description_configure_provider">Для подключения к вашему провайдеру %1$s получает всю необходимую информацию о конфигурации. Это происходит только при первичной настройке.</string>
- <string name="description_configure_provider_circumvention">%1$s получает все необходимые конфигурационные данные от провайдера. Это происходит только при первичной настройке. Вы выбрали использование технологии обхода, поэтому процесс может занять некоторое время.</string>
+ <string name="description_configure_provider_circumvention">%1$s пытается получить все необходимые конфигурационные данные от провайдера. Это происходит только при первичной настройке. Вы выбрали использование технологии обхода, поэтому процесс может занять некоторое время.</string>
<string name="details">Подробности</string>
<string name="tor_status">Статус Tor</string>
<string name="snowflake_status">Статус Snowflake</string>
@@ -233,11 +237,35 @@ https://0xacab.org/leap/bitmask_android/issues</string>
<string name="snowflake_broker_success">Прокси Snowflake успешно согласованы</string>
<string name="snowflake_sending_data">Отправка данных через Snowflake</string>
<string name="title_upcoming_connection_request">Запрос на предстоящее подключение</string>
- <string name="upcoming_connection_request_description">Далее Android напомнит вам, что важно доверять своему провайдеру VPN. Bitmask сотрудничает только с теми провайдерами, которые придерживаются строгих правил конфиденциальности для VPN и имеют подтверждённую историю защиты персональных данных пользователей.</string>
+ <string name="title_upcoming_request">Предстоящие запросы</string>
+ <string name="title_upcoming_request_summary">На следующих панелях Android запросит у вас разрешение в виде запроса на подключение и запроса на уведомление.</string>
+ <string name="title_upcoming_connection_request_summary_custom">Принятие запроса на подключение важно для использования базовой функциональности %s.</string>
+ <string name="title_upcoming_connection_request_summary">Что касается запроса на подключение, важно знать, что Bitmask сотрудничает только с надежными провайдерами, которые придерживаются лучших практик для VPN и имеют подтвержденную историю защиты данных и личных данных пользователей. Однако если вы вручную подключаетесь к непубличному провайдеру, убедитесь, что вы ему доверяете.</string>
+ <string name="title_upcoming_notification_request_summary">Приняв запрос на включение уведомления, приложение будет работать в фоновом режиме и позволит вам видеть расход данных в центре уведомлений Android.
+</string>
<string name="title_upcoming_notifications_request">Запрос на предстоящие уведомления</string>
- <string name="upcoming_notifications_request_description">Далее Android запросит разрешение на отображение уведомлений. Это обеспечит стабильное фоновое подключение и позволит вам видеть использование трафика в шторке уведомлений Android.</string>
<string name="title_setup_success">Всё готово!</string>
<string name="setup_success_description">Нажмите кнопку ниже для подключения</string>
<string name="permission_rejected">Запрос на разрешение отклонён.</string>
- <string name="login_not_supported">Текущая версия приложения не поддерживает возможность входа, поэтому необходимо обновить сертификат VPN для этого провайдера.</string>
+ <string name="login_not_supported">Текущая версия приложения не поддерживает возможность входа, которая необходима для обновления сертификата VPN для этого провайдера.</string>
+ <string name="select_language">Выберите язык</string>
+ <string name="syntax_check">Проверка синтаксиса:</string>
+ <string name="validation_status_success">Хорошо</string>
+ <string name="validation_status_failure">Недопустимый</string>
+ <string name="enter_invite_code">Введите код приглашения</string>
+ <string name="scan_qr_code">Сканировать QR-код</string>
+ <string name="invalid_code"> Неверный код</string>
+ <string name="automatic_bridge">Автоматически (рекомендуется)</string>
+ <string name="automatic_bridge_description">Подключение будет осуществляться с использованием наилучших доступных мостов и протоколов.</string>
+ <string name="manual_bridge">Ручная конфигурация</string>
+ <string name="manual_bridge_description">Выбор приватных мостов и определенных протоколов</string>
+ <string name="censorship_circumvention_description">Ручная настройка требует технических знаний. Действуйте с осторожностью.</string>
+ <string name="discovery">Дискавери</string>
+ <string name="discovery_description">Цензоры могут блокировать получение важной информации о конфигурации от вашего провайдера. Выберите вариант обхода блокировки.</string>
+ <string name="automatically_select">Автоматический выбор</string>
+ <string name="invite_proxy">Приглашение-прокси</string>
+ <string name="tunnelling">Туннель</string>
+ <string name="tunnelling_description">Цензоры могут блокировать доступ к свободному интернету. Выберите вариант обхода блокировки.</string>
+ <string name="port_hopping">Переход через порт</string>
+ <string name="port_hopping_description">Цензоры используют анализ трафика, чтобы блокировать доступ к открытому интернету. Переход через порт может усложнить им эту задачу.</string>
</resources>
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index d7918c34..7fb0690c 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -104,7 +104,7 @@
<string name="warning_corrupted_provider_cert">Kaydedilmiş hizmet sağlayıcı sertifikası geçersiz. %s güncellemeyi (önerilir) ya da ticari bir CA sertifikası kullanarak hizmet sağlayıcı sertifikasını güncellemeyi seçebilirsiniz.</string>
<string name="warning_expired_provider_cert">Kaydedilmiş hizmet sağlayıcı sertifikasının süresi dolmuş. %s güncellemeyi (önerilir) ya da ticari bir CA sertifikası kullanarak hizmet sağlayıcı sertifikasını güncellemeyi seçebilirsiniz.</string>
<string name="downloading_vpn_certificate_failed">VPN sertifikası indirilemedi. Yeniden deneyin ya da başka bir hizmet sağlayıcı seçin.</string>
- <string name="vpn_certificate_is_invalid">VPN sertifikası geçersiz. Yeni bir sertifika indirmeyi deneyin.</string>
+ <string name="vpn_certificate_is_invalid">VPN sertifikanızı yenileme zamanınız geldi. Bağlantınızı güvende tutmak için yeni bir sertifika indirin. Bu güncelleme belirli aralıklarla yapılan normal bir işlemdir.</string>
<string name="vpn_certificate_user_message">VPN sertifikası geçersiz. Lütfen yeni bir sertifika indirmek için oturum açın.</string>
<string name="save_battery">Güç tasarrufu</string>
<string name="subtitle_save_battery">VPN erişim noktası açıkken devre dışı</string>
@@ -211,7 +211,11 @@
<string name="select_provider_description">VPN kullandığınızda, İnternet hizmeti sağlayıcınızına olan güveninizi VPN hizmeti sağlayıcınıza aktarmış olursunuz. Bitmask yalnızca temiz bir gizlilik koruma ve savunma geçmişi olan hizmet sağlayıcılar ile bağlantı kurar.</string>
<string name="provider_description_riseup">Riseup özgürleştirici toplumsal değişim üzerine çalışan kişi ve gruplara çevrim içi iletişim araçları sağlıyor. Projemiz, demokratik alternatifleri oluşturmayı ve kendi denetimimiz altında güvenli iletişim araçları geliştirmeyi amaçlıyor.</string>
<string name="next">Sonraki</string>
- <string name="add_provider_description">Bitmask herkese açık olarak listelenmeyen güvenli hizmet sağlayıcılar ile bağlantı kurar. Hizmet sağlayıcınızın adresini aşağıya yazın.</string>
+ <string name="add_provider_description">Bitmask, herkese açık olarak listelenmeyen hizmet sağlayıcılara bağlanmanızı sağlar. Eklediğiniz hizmet sağlayıcıyı bildiğinizden ve güvendiğinizden emin olun.</string>
+ <string name="add_provider_prompt">Hizmet sağlayıcının adresini buraya yazın.</string>
+ <string name="invite_code_provider_description">Bitmask bir özel davet kodu ile hizmet sağlayıcılara bağlanmanızı sağlar.</string>
+ <string name="invite_code_provider_prompt">Güvenli davet Kodunuzu buraya yazın.</string>
+ <string name="qr_scanner_prompt">Kare kodu tara</string>
<string name="provider_description_calyx">Calyx, İnternet ve mobil iletişim endüstrisinde konuşma özgürlüğü, ifade özgürlüğü, sivil katılım ve gizlilik haklarını özendirmek için gizlilik teknolojileri üzerine çalışan, bunları sınayan, geliştiren ve gerçekleştiren kâr amacı gütmeyen bir eğitim ve araştırma örgütüdür.</string>
<string name="title_circumvention_setup">Engellemeyi aşmanız mı gerekiyor?</string>
<string name="circumvention_setup_description">İnternet\'in engellendiği bir yerde yaşıyorsanız, tüm İnternet hizmetlerine erişmek için engellemeyi aşma seçeneklerimizi kullanabilirsiniz. Bu seçenekler bağlantınızı yavaşlatır!</string>
@@ -230,11 +234,34 @@
<string name="snowflake_broker_success">Snowflake vekil sunucusuna erişildi</string>
<string name="snowflake_sending_data">Veriler Snowflake ile gönderiliyor</string>
<string name="title_upcoming_connection_request">Gelen bağlantı isteği</string>
- <string name="upcoming_connection_request_description">Android, sonraki ekranda VPN hizmeti sağlayıcınıza güvenmenizin temel olduğunu size anımsatacak. Bitmask yalnızca en iyi gizlilik uygulamalarını sıkı bir şekilde uygulayan ve kullanıcıların verileri ile kimliklerini korumakta doğrulanabilir bir geçmişi olan VPN hizmetleri ile işbirliği yapıyor.</string>
+ <string name="title_upcoming_request">İstenecek izinler</string>
+ <string name="title_upcoming_request_summary">Sonraki panolarda Android sizden Bağlantı isteği ve Bildirim isteği ile izninizi isteyecek.</string>
+ <string name="title_upcoming_connection_request_summary_custom">Temel %s işlevselliğini sağlamak için bağlantı isteğinin kabul edilmesi önemlidir.</string>
+ <string name="title_upcoming_connection_request_summary">Bağlantı isteği için, Bitmask uygulamasının yalnızca VPN uygulamaları için iyi örnekleri uygulayan ve kullanıcıların verileri ile kimliklerini koruma konusunda doğrulanabilir bir geçmişi olan güvenilir hizmet sağlayıcılarla işbirliği yaptığını bilmeniz önemlidir. Bununla birlikte, herkese açık olmayan bir hizmet sağlayıcıya el ile bağlanıyorsanız, onun güvenilir olduğundan emin olun.</string>
+ <string name="title_upcoming_notification_request_summary">Bildirim isteğini kabul etmek, uygulamanın arka planda çalışmasına yol açar ve Android bildirim merkezinde veri kullanımınızı görmenizi sağlar.</string>
<string name="title_upcoming_notifications_request">Gelen bildirim isteği</string>
- <string name="upcoming_notifications_request_description">Android, sonraki ekranda bildirimlere izin vermek isteyip istemediğinizi soracak. Böylece kararlı bir arka plan bağlantısı kurulduğundan emin olunacak ve veri kullanımınızı Android bildirim merkezinde görebileceksiniz.</string>
<string name="title_setup_success">Hazırsınız!</string>
<string name="setup_success_description">Bağlanmak için aşağıdaki düğmeye tıklayın</string>
<string name="permission_rejected">İzin isteği reddedildi</string>
<string name="login_not_supported">Bu hizmet sağlayıcı için VPN sertifikanızı güncellemeniz gerektiğinden geçerli uygulama sürümü oturum açmayı desteklemiyor. </string>
+ <string name="select_language">Dil seçin</string>
+ <string name="syntax_check">Yazım denetimi:</string>
+ <string name="validation_status_success">İyi</string>
+ <string name="validation_status_failure">Kötü</string>
+ <string name="enter_invite_code">Davet kodunu yazın</string>
+ <string name="scan_qr_code">Kare kodu tara</string>
+ <string name="invalid_code">Kod geçersiz</string>
+ <string name="automatic_bridge">Otomatik (önerilen)</string>
+ <string name="automatic_bridge_description">Bağlantı kullanılabilecek en iyi köprüler ve iletişim kuralları ile kurulmaya çalışılacak.</string>
+ <string name="manual_bridge">El ile yapılandırma</string>
+ <string name="manual_bridge_description">Özel köprüler ve belirli iletişim kuralları seçin</string>
+ <string name="censorship_circumvention_description">El ile yapılandırma için teknik bilgi gereklidir. Dikkatli davranın.</string>
+ <string name="discovery">Keşif</string>
+ <string name="discovery_description">Sansür uygulayıcılar, hizmet sağlayıcınızın sunduğu kritik yapılandırma bilgisinin keşfini engelleyebilir. Engellemeleri aşmak için bir seçenek seçin.</string>
+ <string name="automatically_select">Otomatik olarak seçilsin</string>
+ <string name="invite_proxy">Vekil sunucu daveti</string>
+ <string name="tunnelling">Tünelleme</string>
+ <string name="tunnelling_description">Sansür uygulayıcılar açık internete erişimi engelleyebilir. Engellemeleri aşmak için bir seçenek seçin.</string>
+ <string name="port_hopping">Bağlantı noktası değiştirme</string>
+ <string name="port_hopping_description">Sansür uygulayıcılar açık internete erişimi engellemek için trafik analizini kullanırlar. Bağlantı noktası değiştirme bunu onlar için zorlaştırır.</string>
</resources>
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 34979153..b6fd056a 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -1,44 +1,179 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="retry">Повторити</string>
+ <string name="repository_url_text">Вихідний код доступний на: https://0xacab.org/leap/bitmask_android</string>
+ <string name="leap_tracker">Відстеження проблем доступно на https://0xacab.org/leap/bitmask_android/issues</string>
+ <string name="translation_project_text">Ми вітаємо перекладацькі зусилля. Перегляньте проєкт на Transifex: https://www.transifex.com/projects/p/bitmask/</string>
+ <string name="switch_provider_menu_option">Змінити постачальника</string>
<string name="info">Інформація</string>
<string name="show_connection_details">Показати відомості про підключення</string>
+ <string name="connection_details">Відомості про підключення</string>
<string name="routes_info">Маршрути: %s</string>
+ <string name="routes_info6">Маршрути IPv6: %s</string>
<string name="error_empty_username">Ім\'я користувача не може бути порожнім.</string>
+ <string name="provider_label">Постачальник:</string>
+ <string name="provider_label_none">Постачальника не налаштовано</string>
+ <string name="status_unknown">Стан невідомий.</string>
+ <string name="eip_service_label">Шифрований VPN для доступу до інтернету</string>
+ <string name="configuration_wizard_title">Обрати постачальника послуг</string>
+ <string name="add_provider">Додати нового постачальника</string>
+ <string name="introduce_new_provider">Додати нового постачальника послуг</string>
<string name="save">Зберегти</string>
<string name="new_provider_uri">Назва домена</string>
+ <string name="valid_url_entered">URL-адреса правильна</string>
+ <string name="not_valid_url_entered">URL-адреса неправильна</string>
+ <string name="provider_details_title">Деталі постачальника</string>
+ <string name="use_anonymously_button">Використовувати анонімно</string>
<string name="username_hint">ім\'я користувача</string>
+ <string name="username_ask">Будь ласка, введіть ім\'я користувача</string>
<string name="password_ask">Будь ласка, введіть свій пароль</string>
<string name="password_hint">пароль</string>
- <string name="password_match">Паролі співпадають</string>
- <string name="password_mismatch">Паролі не співпадають</string>
+ <string name="password_match">Паролі збігаються</string>
+ <string name="password_mismatch">Паролі не збігаються</string>
+ <string name="user_message">Повідомлення користувача</string>
<string name="about_fragment_title">Про</string>
- <string name="error_bad_user_password_user_message">Неправильний логін або пароль</string>
+ <string name="exclude_apps_fragment_title">Виключити застосунки з VPN</string>
+ <string name="error_srp_math_error_user_message">Спробуйте знов: Помилка підрахунку сервера</string>
+ <string name="error_bad_user_password_user_message">Неправильне ім\'я користувача або пароль</string>
+ <string name="error_not_valid_password_user_message">Він має бути щонайменше 8 символів</string>
+ <string name="error_client_http_user_message">Спробуйте знов: Помилка клієнта HTTP</string>
+ <string name="error_io_exception_user_message">Спробуйте знов: Помилка введення / виведення</string>
+ <string name="error_json_exception_user_message">Спробуйте знов: Неправильна відповідь від сервера</string>
+ <string name="error_no_such_algorithm_exception_user_message">Алгоритм шифрування не знайдено. Будь ласка, оновіть Android!</string>
+ <string name="signup_or_login_button">Зареєструватись / Увійти</string>
<string name="login_button">Вхід у систему</string>
+ <string name="login_to_profile">Увійти до облікового запису</string>
<string name="logout_button">Вийти</string>
+ <string name="signup_button">Зареєструватись</string>
+ <string name="create_profile">Створити обліковий запис</string>
+ <string name="setup_provider">Налаштувати постачальника</string>
+ <string name="setup_error_title">Помилка налаштування</string>
<string name="setup_error_configure_button">Налаштування</string>
<string name="setup_error_close_button">Вихід</string>
+ <string name="server_unreachable_message">Сервер недоступний, будь ласка, спробуйте знов.</string>
+ <string name="error.security.pinnedcertificate">Помилка безпеки, оновіть застосунок або оберіть іншого постачальника.</string>
+ <string name="malformed_url">Здається, це не постачальник %s.</string>
+ <string name="certificate_error">Це не довірений постачальник %s.</string>
+ <string name="service_is_down_error">Служба не працює.</string>
+ <string name="configuring_provider">Налаштування постачальника</string>
+ <string name="incorrectly_downloaded_certificate_message">Ваш анонімний сертифікат не завантажено</string>
+ <string name="downloading_certificate_message">Завантаження сертифіката VPN</string>
+ <string name="updating_certificate_message">Оновлення сертифіката VPN</string>
+ <string name="login.riseup.warning">Користувачам Riseup потрібно створити окремий обліковий запис задля користування службою VPN</string>
<string name="succesful_authentication_message">Автентифіковано</string>
<string name="authentication_failed_message">Збій автентифікації</string>
+ <string name="registration_failed_message">Реєстрація не вдалась</string>
+ <string name="eip_status_start_pending">Починаємо з\'єднання</string>
+ <string name="eip_status_connecting">Під\'єднуємось до VPN</string>
+ <string name="eip_status_unsecured">Незахищене з\'єднання</string>
+ <string name="eip_status_secured">Захищене з\'єднання</string>
+ <string name="eip_cancel_connect_title">Скасувати з\'єднання?</string>
+ <string name="eip_cancel_connect_text">Зараз відбувається спроба з\'єднання. Бажаєте скасувати її?</string>
+ <string name="eip.warning.browser_inconsistency">Вимкнути з\'єднання VPN? Коли VPN вимкнено, ваша персональна інформація може потрапити до постачальника послуг інтернету або локальної мережі.</string>
+ <string name="eip_state_not_connected">Не працює! Небезпечне з\'єднання!</string>
+ <string name="eip_state_connected">Безпечне з\'єднання</string>
+ <string name="provider_problem">Схоже на проблему у постачальника.</string>
+ <string name="try_another_provider">Будь ласка, спробуйте іншого постачальника, або сконтактуйте зі своїм. </string>
<string name="default_username">Анонімно</string>
+ <string name="logging_in">Вхід</string>
+ <string name="signing_up">Реєстрація</string>
+ <string name="vpn.button.turn.on">Увімкнути</string>
+ <string name="vpn.button.turn.off">Вимкнути</string>
+ <string name="vpn_button_turn_off_blocking">Не блокувати</string>
+ <string name="vpn_securely_routed">Ваш трафік безпечно перенаправляється через:</string>
+ <string name="vpn_securely_routed_no_internet">Відсутнє з\'єднання з інтернетом, щойно воно з\'явиться – ваш трафік безпечно перенаправлятиметься через:</string>
<string name="log_fragment_title">Журнал</string>
<string name="vpn_fragment_title">VPN</string>
<string name="navigation_drawer_open">Відкрити панель навігації</string>
<string name="navigation_drawer_close">Закрити панель навігації</string>
<string name="action_settings">Налаштування</string>
- <string name="always_on_vpn">Завжди-VPN</string>
+ <string name="void_vpn_establish">%s блокує увесь вихідний трафік інтернету.</string>
+ <string name="void_vpn_error_establish">Блокування всього трафіку інтернету не вдалось.</string>
+ <string name="void_vpn_stopped">Блокування вихідного трафіку припинилось.</string>
+ <string name="void_vpn_title">Блокування трафіку</string>
+ <string name="update_provider_details">Оновити дані постачальника</string>
+ <string name="update_certificate">Оновити сертифікат</string>
+ <string name="warning_eip_json_corrupted">Оновлення налаштувань постачальника не вдалось.</string>
+ <string name="vpn_certificate_is_invalid">Сертифікат VPN недійсний. Спробуйте завантажити новий.</string>
+ <string name="vpn_certificate_user_message">Сертифікат VPN недійсний. Будь ласка, увійдіть для завантаження нового сертифікату.</string>
+ <string name="save_battery">Економія заряду</string>
+ <string name="subtitle_save_battery">Недоступне під час використання Точки доступу VPN</string>
+ <string name="always_on_vpn">VPN завжди увімкнено</string>
+ <string name="subtitle_always_on_vpn">Відкрити системні налаштування Android</string>
+ <string name="tethering">Точка доступу VPN</string>
+ <string name="ipv6Firewall">Блокувати IPv6</string>
+ <string name="require_root">Вимагає прав кореневого доступу</string>
+ <string name="show_experimental">Показувати експериментальні можливості</string>
+ <string name="hide_experimental">Приховати експериментальні можливості</string>
+ <string name="experimental_features">Експериментальні можливості</string>
+ <string name="tethering_enabled_message">Будь ласка, спочатку впевніться, що можливість використання комп\'ютером мобільного пристрою як точки доступу увімкнено у <![CDATA[<b>системних налаштуваннях</b>]]>.</string>
+ <string name="tethering_message">Поділитись VPN з іншими пристроями через:</string>
<string name="tethering_wifi">Точка доступу Wi-Fi</string>
+ <string name="tethering_usb">Використання мобільного пристрою як модема через USB</string>
+ <string name="tethering_bluetooth">Використання мобільного пристрою як модема через Bluetooth</string>
+ <string name="do_not_show_again">Не показувати знов</string>
<string name="donate_title">Пожертвування</string>
<string name="donate_button_remind_later">Нагадайте мені пізніше</string>
<string name="donate_button_donate">Пожертвування</string>
- <string name="nav_drawer_obfuscated_connection">Використати Мости</string>
+ <string name="obfuscated_connection">Використовувати замасковане з\'єднання.</string>
+ <string name="obfuscated_connection_try">Спроба встановлення замаскованого з\'єднання.</string>
+ <string name="nav_drawer_obfuscated_connection">Використати мости</string>
+ <string name="nav_drawer_subtitle_obfuscated_connection">Обходити фільтрування VPN</string>
+ <string name="warning_option_try_best">Обрати найкраще місцеперебування</string>
+ <string name="warning_option_try_pt">Обрати замасковане з\'єднання</string>
+ <string name="warning_option_try_ovpn">Обрати стандартне з\'єднання</string>
+ <string name="version_update_found">Торкніться для завантаження.</string>
+ <string name="version_update_title">Знайдено нову версію %s.</string>
+ <string name="version_update_apk_description">Завантаження нової версії %s</string>
+ <string name="version_update_download_title">Нова версія %s завантажена.</string>
+ <string name="version_update_download_description">Торкніться для встановлення оновлення.</string>
+ <string name="version_update_error_pgp_verification">Помилка перевірки PGP. Завантаження ігнороване.</string>
+ <string name="version_update_error">Оновлення не вдалось.</string>
+ <string name="version_update_error_permissions">Відсутні дозволи на встановлення застосунку.</string>
+ <string name="gateway_selection_title">Вибрати місцеперебування</string>
+ <string name="gateway_selection_recommended_location">Рекомендоване місцеперебування</string>
+ <string name="gateway_selection_recommended">Рекомендоване</string>
+ <string name="gateway_selection_manually">Обрати вручну</string>
+ <string name="gateway_selection_automatic_location">Автоматично використовувати найкраще з\'єднання</string>
<string name="gateway_selection_automatic">Автоматично</string>
+ <string name="reconnecting">З\'єднання знов...</string>
+ <string name="tor_starting">Наводимо мости для обходу цензурування...</string>
+ <string name="tor_stopping">Розводимо мости</string>
+ <string name="tor_started">Використовуємо мости для обходу цензурування</string>
+ <string name="log_conn_done_pt">З\'єднано з додатковим передавачем</string>
+ <string name="log_conn_pt">З\'єднання з додатковим передавачем</string>
+ <string name="log_conn_done">Під\'єднано до ретранслятора</string>
<string name="log_onehop_create">Встановлення зашифрованого з\'єднання до каталогу</string>
<string name="log_loading_keys">Завантаження сертифікатів авторизації</string>
<string name="log_circuit_create">Створення ланцюга Tor </string>
<string name="log_done">Запуск</string>
+ <string name="channel_name_tor_service">Служба мостів %s</string>
+ <string name="channel_description_tor_service">Інформує про використання мостів під час налаштування %s.</string>
<string name="hide">Приховати</string>
+ <string name="error_network_connection">%s не має інтернет з\'єднання. Будь ласка, перевірте налаштування WiFi та стільникових даних.</string>
+ <string name="censorship_circumvention">Обхід цензурування</string>
<string name="use_snowflake">Використовувати Snowflake</string>
+ <string name="vpn_settings">Налаштування VPN</string>
+ <string name="prefer_udp">Використовувати UDP за можливості</string>
<string name="advanced_settings">Додаткові налаштування</string>
<string name="cancel_connection">Від\'єднати</string>
+ <string name="unknown_location">Невідоме місцеперебування</string>
+ <string name="splash_footer">Розроблено LEAP</string>
+ <string name="welcome">Ласкаво просимо!</string>
+ <string name="select_provider">Обрати постачальника</string>
+ <string name="next">Далі</string>
+ <string name="title_circumvention_setup">Чи потрібен Вам обхід цензури?</string>
+ <string name="circumvention_setup_description">Якщо ви живете там, де Інтернет цензурується, ви можете скористатися нашими параметрами обходу цензури, щоб отримати доступ до всіх інтернет-сервісів. Ці налаштування сповільнять ваше з’єднання!</string>
+ <string name="circumvention_setup_hint">%s автоматично намагатиметься під\'єднати вас до Інтернету за допомогою різноманітних технологій обходу цензури. Ви можете додатково налаштувати це в розширених налаштуваннях.</string>
+ <string name="use_standard_vpn">Використовувати стандартний %s</string>
+ <string name="use_circumvention_tech">Використовувати способи обходи цензури (повільніше)</string>
+ <string name="description_configure_provider">Для з\'єднання з вашим постачальником послуг %1$s отримує усю інформацію, необхідну для з\'єднання. Це відбувається лише під час першого налаштування.</string>
+ <string name="details">Деталі</string>
+ <string name="tor_status">Статус Tor</string>
+ <string name="snowflake_status">Статус Snowflake</string>
+ <string name="snowflake_started">Клієнт Snowflake запущений</string>
+ <string name="snowflake_sending_data">Пересилання даних через Snowflake</string>
+ <string name="title_setup_success">Усе налаштовано!</string>
+ <string name="setup_success_description">Натисніть на кнопку нижче для з\'єднання</string>
+ <string name="permission_rejected">Запит на дозвіл відхилено.</string>
</resources>
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index 1462179c..4914d69f 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -202,6 +202,9 @@
<string name="cancel_connection">Ngắt kết nối</string>
<string name="welcome">Chào mừng bạn!</string>
<string name="next">Tiếp</string>
+ <string name="qr_scanner_prompt">Quét mã QR</string>
<string name="details">Chi Tiết</string>
<string name="tor_status">Trạng thái Tor</string>
+ <string name="select_language">Chọn Ngôn Ngữ</string>
+ <string name="scan_qr_code">Quét mã QR</string>
</resources>
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 1024be39..5c77ccac 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -206,7 +206,37 @@
<string name="unknown_location">未知的地理位置</string>
<string name="splash_footer">由 LEAP 開發</string>
<string name="welcome">歡迎!</string>
+ <string name="select_provider">選擇供應商</string>
+ <string name="select_provider_description">使用 VPN 是將信任從網路服務供應商轉移到 VPN 提供者。 Bitmask 僅與明確保護和倡導隱私的提供者建立連線。</string>
+ <string name="provider_description_riseup">Riseup 致力於為解放社會變革的人民和團體提供線上交流工具。 我們透過控制自己的安全通訊方式來創造民主替代方案並實踐自決計畫。</string>
<string name="next">下一個</string>
+ <string name="add_provider_description">Bitmask 連接到未公開的可信任提供者。 在下面輸入提供者的網址。</string>
+ <string name="qr_scanner_prompt">掃描二維條碼</string>
+ <string name="provider_description_calyx">Calyx 為非營利教育和研究組織,致力於研究、測試、開發和實施隱私技術和工具,以促進網路和行動通訊產業的言論自由、表達自由、公民參與和隱私權。</string>
+ <string name="title_circumvention_setup">需規避審查?</string>
+ <string name="circumvention_setup_description">如果住在網路審查的地區,可以使用我們的審查規避工具來存取所有網路服務。 這些選項會減慢您的連線速度!</string>
+ <string name="circumvention_setup_hint">%s 會自動嘗試使用各種規避技術來連接網際網路,進階設定中可對此進行微調。</string>
+ <string name="use_standard_vpn">使用標準 %s</string>
+ <string name="use_circumvention_tech">使用審查規避技術 (較慢)</string>
+ <string name="description_configure_provider">連接到供應商%1$s取得所有必要的設定資訊。 這僅發生在初次設定。</string>
+ <string name="description_configure_provider_circumvention">%1$s 嘗試從供應商收集所需的配置資料。 這僅發生在初次設定。 因選用規避技術,可能需要一些時間。</string>
<string name="details">詳情</string>
<string name="tor_status">洋蔥路由狀態</string>
+ <string name="snowflake_status">Snowflake 狀態</string>
+ <string name="snowflake_started">Snowflake 用戶端啟動</string>
+ <string name="snowflake_negotiating_rendezvous_http">協商 Snowflake 代理滙合點 (http)</string>
+ <string name="snowflake_negotiating_rendezvous_amp_cache">協商 Snowflake 代理集合點 (amp 快取)</string>
+ <string name="snowflake_socks_error">Snowflake SOCKS 錯誤</string>
+ <string name="snowflake_broker_success">Snowflake 代理握手成功</string>
+ <string name="snowflake_sending_data">透過 Snowflake 發送數據</string>
+ <string name="title_upcoming_connection_request">即將到來的連接請求</string>
+ <string name="title_upcoming_notifications_request">即將到來通知請求</string>
+ <string name="title_setup_success">一切就緒!</string>
+ <string name="setup_success_description">點擊下方按鈕來接線</string>
+ <string name="permission_rejected">授權請求被拒。</string>
+ <string name="login_not_supported">應用程式當前的版本不支援登入,須為此供應商更新 VPN 憑證。</string>
+ <string name="select_language">選取語言</string>
+ <string name="validation_status_success">優</string>
+ <string name="validation_status_failure">不良</string>
+ <string name="scan_qr_code">掃描二維條碼</string>
</resources>
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index c88a7ada..d0901e88 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -207,10 +207,19 @@
<string name="splash_footer">由 LEAP 开发</string>
<string name="welcome">欢迎!</string>
<string name="select_provider">选择提供商</string>
+ <string name="select_provider_description">使用 VPN 是将信任从网路服务供应商转移到 VPN 提供者。 Bitmask 只和有良好记录保护和倡导隐私的提供者建立连线。</string>
+ <string name="provider_description_riseup">Riseup 为解放社会变革的人民和团体提供线上交流工具,透过控制自己的安全通讯方式来创造民主替代方案并实践自决方案。</string>
<string name="next">下一步</string>
+ <string name="add_provider_description">Bitmask 连接未公开的可信任提供者。 在下面输入提供者的网址。</string>
+ <string name="qr_scanner_prompt">扫描二维码</string>
+ <string name="provider_description_calyx">Calyx 为非营利教育研究组织,致力研究、测试、开发和实施隐私技术和工具,促进网路和行动通讯产业的言论自由、表达自由、公民参与和隐私权。</string>
<string name="title_circumvention_setup">需要进行审查规避?</string>
+ <string name="circumvention_setup_description">如果居住在网路审查的地区,可以使用我们的审查规避工具来存取所有网路服务。 但这些选项会减慢连线速度!</string>
+ <string name="circumvention_setup_hint">%s会自动尝试使用各种规避技术来连接互联网,高级设定下可对此作微调。</string>
<string name="use_standard_vpn">使用标准 %s</string>
<string name="use_circumvention_tech">使用审查规避技术(慢速)</string>
+ <string name="description_configure_provider">连接到供应商%1$s取得必要的设定资讯。 这仅发生在首次设定。</string>
+ <string name="description_configure_provider_circumvention">%1$s 试图向供应商收集所需的配置数据。 这只发生在初次设定。 因选用规避技术,可能要多花点时间。</string>
<string name="details">详情</string>
<string name="tor_status">Tor 状态</string>
<string name="snowflake_status">Snowflake 状态</string>
@@ -225,4 +234,7 @@
<string name="title_setup_success">一切就绪!</string>
<string name="setup_success_description">点按下方按钮进行连接</string>
<string name="permission_rejected">权限请求被拒绝。</string>
+ <string name="login_not_supported">应用程序当前的版本不支援登入,须为此供应商更新 VPN 凭证。</string>
+ <string name="select_language">选择语言</string>
+ <string name="scan_qr_code">扫描二维码</string>
</resources>
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 84a2d9f0..d81c4021 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -4,6 +4,7 @@
<color name="colorPrimaryLight">#FF69B4</color>
<color name="colorPrimaryDark">#ef0072</color>
<color name="colorPrimary_transparent">#0D000000</color>
+ <color name="color_provider_description_background">#F6F6F6</color>
<color name="colorPrimary_transparent_dark">#1F000000</color>
<color name="colorBackground">#fffafafa</color>
<color name="colorError">#ef9a9a</color>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index edbe56bb..41904f13 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -104,7 +104,7 @@
<string name="warning_corrupted_provider_cert">Stored provider certificate is invalid. You can either update %s (recommended) or update the provider certificate using a commercial CA certificate.</string>
<string name="warning_expired_provider_cert">Stored provider certificate is expired. You can either update %s (recommended) or update the provider certificate using a commercial CA certificate.</string>
<string name="downloading_vpn_certificate_failed">Downloading the VPN certificate failed. Try again or choose another provider.</string>
- <string name="vpn_certificate_is_invalid">VPN certificate is invalid. Try to download a new one.</string>
+ <string name="vpn_certificate_is_invalid">It\'s time to update your VPN certificate. Download a new certificate to keep your connection secure. This is a routine update.</string>
<string name="vpn_certificate_user_message">The VPN certificate is invalid. Please log in to download a new one.</string>
<string name="save_battery">Save battery</string>
<string name="subtitle_save_battery">Disabled while VPN Hotspot is on</string>
@@ -211,7 +211,11 @@
<string name="select_provider_description">When using a VPN you are transferring your trust from your Internet Service Provider to your VPN provider. Bitmask only connects to providers with a clear history of privacy protection and advocacy.</string>
<string name="provider_description_riseup">Riseup provides online communication tools for people and groups working on liberatory social change. We are a project to create democratic alternatives and practice self-determination by controlling our own secure means of communications.</string>
<string name="next">Next</string>
- <string name="add_provider_description">Bitmask connects to trusted providers that are not publicly listed. Enter your provider’s url below.</string>
+ <string name="add_provider_description">Bitmask allows you to connect to providers that are not publicly listed. Make sure you know and trust the provider you are adding.</string>
+ <string name="add_provider_prompt">Enter the provider’s URL here.</string>
+ <string name="invite_code_provider_description">Bitmask allows you to connect to providers using a private Invite Code. </string>
+ <string name="invite_code_provider_prompt">Enter your trusted Invite Code here.</string>
+ <string name="qr_scanner_prompt">Scan QR Code</string>
<string name="provider_description_calyx">Calyx is a non-profit education and research organization devoted to studying, testing, developing and implementing privacy technology and tools to promote free speech, free expression, civic engagement and privacy rights on the internet and in the mobile communications industry.</string>
<string name="title_circumvention_setup">Do You Require Censorship Circumvention?</string>
<string name="circumvention_setup_description">If you live where the internet is censored you can use our censorship circumvention options to access all internet services. These options will slow down your connection!</string>
@@ -231,11 +235,34 @@
<string name="snowflake_broker_success">Snowflake proxy rendezvous successful</string>
<string name="snowflake_sending_data">Sending data via Snowflake</string>
<string name="title_upcoming_connection_request">Upcoming Connection Request</string>
- <string name="upcoming_connection_request_description">In the next panel Android will remind you that it’s essential to trust your VPN provider. Bitmask only partners with providers that adhere to strict privacy best practices for VPNs and have a verifiable history of protecting user’s data and identities.</string>
+ <string name="title_upcoming_request">Upcoming requests</string>
+ <string name="title_upcoming_request_summary">In the next panels Android will ask for your permission in form of a Connection Request and Notification Request.</string>
+ <string name="title_upcoming_connection_request_summary_custom">Accepting the Connection Request is essential to use the core functionality of %s.</string>
+ <string name="title_upcoming_connection_request_summary">For the Connection Request it’s important to know that Bitmask only partners with trusted partner providers that adhere to best practices for VPNs and have a verifiable history of protecting user’s data and identities. However, if you are manually connecting to a non-public provider, make sure you trust them.</string>
+ <string name="title_upcoming_notification_request_summary">Accepting the Notification Request allows the app to run in the background and enables you to see your data usage from within Android’s notification center.</string>
<string name="title_upcoming_notifications_request">Upcoming Notifications Request</string>
- <string name="upcoming_notifications_request_description">In the next panel Android will ask if you want to allow notifications. This will ensure a stable background connection and enable you to see your data usage from within Android’s notification center.</string>
<string name="title_setup_success">You\'re all set!</string>
<string name="setup_success_description">Click the button below to connect</string>
<string name="permission_rejected">Permission request rejected.</string>
<string name="login_not_supported">The current app version doesn\'t support logins, which you need to update your VPN certificate for this provider.</string>
+ <string name="select_language">Select Language</string>
+ <string name="syntax_check">Syntax Check:</string>
+ <string name="validation_status_success">Good</string>
+ <string name="validation_status_failure">Bad</string>
+ <string name="enter_invite_code">Enter invite Code</string>
+ <string name="scan_qr_code">Scan QR Code</string>
+ <string name="invalid_code">Invalid code</string>
+ <string name="automatic_bridge">Automatic (recommended)</string>
+ <string name="automatic_bridge_description">Connection will be attempted using the best available bridges and protocols.</string>
+ <string name="manual_bridge">Manual Configuration</string>
+ <string name="manual_bridge_description">Select private bridges and specific protocols</string>
+ <string name="censorship_circumvention_description">Manual configuration requires technical understanding. Proceed with caution.</string>
+ <string name="discovery">Discovery</string>
+ <string name="discovery_description">Censors can block the discovery of critical configuration information from your provider. Choose a circumvention option to bypass blocks.</string>
+ <string name="automatically_select">Automatically select</string>
+ <string name="invite_proxy">Invite Proxy</string>
+ <string name="tunnelling">Tunneling</string>
+ <string name="tunnelling_description">Censors can block access to the open internet. Choose a circumvention option to bypass blocks.</string>
+ <string name="port_hopping">Port Hopping</string>
+ <string name="port_hopping_description">"Censors use traffic analysis to block access to the open internet. Port Hopping can make this harder for them. "</string>
</resources>
diff --git a/app/src/main/res/values/untranslatable.xml b/app/src/main/res/values/untranslatable.xml
index 80cbf7f3..3d499e85 100644
--- a/app/src/main/res/values/untranslatable.xml
+++ b/app/src/main/res/values/untranslatable.xml
@@ -49,4 +49,32 @@
<!-- gateway selector, move to strings.xml, once the wording is clear -->
<string name="no_location" translatable="false">---</string>
+ <string-array name="supported_languages" translatable="false">
+ <item>cs</item>
+ <item>lt</item>
+ <item>tr</item>
+ <item>pt-BR</item>
+ <item>el</item>
+ <item>en</item>
+ <item>nl</item>
+ <item>de</item>
+ <item>zh-TW</item>
+ <item>ru</item>
+ <item>ar</item>
+ <item>es</item>
+ <item>es-CU</item>
+ <item>zh</item>
+ <item>ja</item>
+ <item>fr</item>
+ <item>fi</item>
+ <item>es-AR</item>
+ <item>fa-IR</item>
+ <item>he</item>
+ <item>uk</item>
+ <item>hu</item>
+ <item>vi</item>
+ </string-array>
+ <string name="tunnelling_obfs4" translatable="false">Obfs4</string>
+ <string name="tunnelling_obfs4_kcp" translatable="false">Obfs4+KCP</string>
+ <string name="tunnelling_quic" translatable="false">Quic</string>
</resources>
diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml
new file mode 100644
index 00000000..48baa6fc
--- /dev/null
+++ b/app/src/main/res/xml/locales_config.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
+ <locale android:name="cs"/>
+ <locale android:name="lt"/>
+ <locale android:name="tr"/>
+ <locale android:name="pt-BR"/>
+ <locale android:name="el"/>
+ <locale android:name="nl"/>
+ <locale android:name="de"/>
+ <locale android:name="en"/>
+ <locale android:name="zh-TW"/>
+ <locale android:name="ru"/>
+ <locale android:name="ar"/>
+ <locale android:name="es"/>
+ <locale android:name="es-CU"/>
+ <locale android:name="zh"/>
+ <locale android:name="ja"/>
+ <locale android:name="fr"/>
+ <locale android:name="fi"/>
+ <locale android:name="es-AR"/>
+ <locale android:name="fa-IR"/>
+ <locale android:name="he"/>
+ <locale android:name="uk"/>
+ <locale android:name="hu"/>
+ <locale android:name="vi"/>
+</locale-config>
diff --git a/app/src/normal/AndroidManifest.xml b/app/src/normal/AndroidManifest.xml
new file mode 100644
index 00000000..3987d95d
--- /dev/null
+++ b/app/src/normal/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+ <uses-feature
+ android:name="android.hardware.camera"
+ android:required="false" />
+ <uses-permission android:name="android.permission.CAMERA"/>
+</manifest> \ No newline at end of file
diff --git a/app/src/normal/assets/riseup.net.json b/app/src/normal/assets/riseup.net.json
index 5e14abc3..407e2e22 100644
--- a/app/src/normal/assets/riseup.net.json
+++ b/app/src/normal/assets/riseup.net.json
@@ -1,37 +1,37 @@
{
- "api_uri":"https://api.black.riseup.net:4430",
- "api_version":"3",
- "ca_cert_fingerprint":"SHA256: dd919b7513b4a1368faa20e38cd3314156805677f48b787cdd9b4a92dec64eb0",
- "ca_cert_uri":"https://black.riseup.net/ca.crt",
- "default_language":"en",
- "description":{
- "en": "Riseup is a non-profit collective in Seattle that provides online communication tools for people and groups working toward liberatory social change."
+ "api_uri": "https://api.black.riseup.net:4430",
+ "api_version": "3",
+ "ca_cert_fingerprint": "SHA256: dd919b7513b4a1368faa20e38cd3314156805677f48b787cdd9b4a92dec64eb0",
+ "ca_cert_uri": "https://black.riseup.net/ca.crt",
+ "default_language": "en",
+ "description": {
+ "en": "Riseup Networks"
},
- "domain":"riseup.net",
- "enrollment_policy":"open",
- "languages":[
+ "domain": "riseup.net",
+ "enrollment_policy": "open",
+ "languages": [
"en"
],
- "name":{
- "en":"Riseup Networks"
+ "name": {
+ "en": "Riseup Networks"
},
- "service":{
- "allow_anonymous":true,
- "allow_free":true,
- "allow_limited_bandwidth":false,
- "allow_paid":false,
- "allow_registration":false,
- "allow_unlimited_bandwidth":true,
- "bandwidth_limit":102400,
- "default_service_level":1,
- "levels":{
- "1":{
- "description":"Please donate.",
- "name":"free"
+ "service": {
+ "allow_anonymous": true,
+ "allow_free": true,
+ "allow_limited_bandwidth": false,
+ "allow_paid": false,
+ "allow_registration": false,
+ "allow_unlimited_bandwidth": true,
+ "bandwidth_limit": 102400,
+ "default_service_level": 1,
+ "levels": {
+ "1": {
+ "description": "Please donate.",
+ "name": "free"
}
}
},
- "services":[
+ "services": [
"openvpn"
]
} \ No newline at end of file
diff --git a/app/src/normal/java/se/leap/bitmaskclient/providersetup/helpers/QrScannerHelper.java b/app/src/normal/java/se/leap/bitmaskclient/providersetup/helpers/QrScannerHelper.java
new file mode 100644
index 00000000..88046360
--- /dev/null
+++ b/app/src/normal/java/se/leap/bitmaskclient/providersetup/helpers/QrScannerHelper.java
@@ -0,0 +1,39 @@
+package se.leap.bitmaskclient.providersetup.helpers;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.fragment.app.Fragment;
+
+import com.journeyapps.barcodescanner.ScanContract;
+import com.journeyapps.barcodescanner.ScanOptions;
+
+import se.leap.bitmaskclient.base.models.Introducer;
+import se.leap.bitmaskclient.providersetup.fragments.helpers.AbstractQrScannerHelper;
+
+public class QrScannerHelper extends AbstractQrScannerHelper {
+
+ private final ActivityResultLauncher<ScanOptions> scannerActivityResultLauncher;
+
+ public QrScannerHelper(Fragment fragment, ScanResultCallback callback) {
+ super(fragment, callback);
+ this.scannerActivityResultLauncher = fragment.registerForActivityResult(new ScanContract(), result -> {
+ if(result.getContents() != null) {
+ try {
+ Introducer introducer = Introducer.fromUrl(result.getContents());
+ callback.onScanResult(introducer);
+ } catch (Exception e) {
+ e.printStackTrace();
+ //binding.editCustomProvider.setText(result.getContents());
+ }
+ }
+ });
+ }
+
+ @Override
+ public void startScan() {
+ ScanOptions options = new ScanOptions();
+ options.setBeepEnabled(false);
+ options.setBarcodeImageEnabled(false);
+ options.setOrientationLocked(false);
+ scannerActivityResultLauncher.launch(options);
+ }
+}
diff --git a/app/src/normalProductionFatDebug/assets/demo.bitmask.net.json b/app/src/normalProductionFatDebug/assets/demo.bitmask.net.json
deleted file mode 100644
index f5998c9b..00000000
--- a/app/src/normalProductionFatDebug/assets/demo.bitmask.net.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "api_uri":"https://api.demo.bitmask.net:4430",
- "api_version":"3",
- "ca_cert_fingerprint":"SHA256: 40ed9d9c13872c1fcba25abdcf26a7b1bdeded433d74fbe1ccb58fbaaab58e23",
- "ca_cert_uri":"https://api.demo.bitmask.net/ca.crt",
- "default_language":"en",
- "description":{
- "en":"This is a demo provider"
- },
- "domain":"demo.bitmask.net",
- "enrollment_policy":"open",
- "languages":[
- "en"
- ],
- "name":{
- "en":"Demo provider"
- },
- "service":{
- "allow_anonymous":true,
- "allow_free":true,
- "allow_limited_bandwidth":false,
- "allow_paid":false,
- "allow_registration":false,
- "allow_unlimited_bandwidth":true,
- "bandwidth_limit":102400,
- "default_service_level":1,
- "levels":{
- "1":{
- "description":"Please donate.",
- "name":"free"
- }
- }
- },
- "services":[
- "openvpn"
- ]
-} \ No newline at end of file
diff --git a/app/src/normalProductionFatDebug/assets/demo.bitmask.net.pem b/app/src/normalProductionFatDebug/assets/demo.bitmask.net.pem
deleted file mode 100644
index f1ede7ab..00000000
--- a/app/src/normalProductionFatDebug/assets/demo.bitmask.net.pem
+++ /dev/null
@@ -1,10 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIBYTCCAQigAwIBAgIBATAKBggqhkjOPQQDAjAXMRUwEwYDVQQDEwxMRUFQIFJv
-b3QgQ0EwHhcNMjQwMjIxMTEzMTUwWhcNMjkwMjIxMTEzNjUwWjAXMRUwEwYDVQQD
-EwxMRUFQIFJvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARKTm8AKkqK
-aMI7dEarRRGEOPa3i49YE4bGNHxO97h14urXOROJWjnwHJdJ3dJk16oR0HKohXR7
-jSxyukoonJkgo0UwQzAOBgNVHQ8BAf8EBAMCAqQwEgYDVR0TAQH/BAgwBgEB/wIB
-ATAdBgNVHQ4EFgQUMVywfKRY9Ec3n98PVIEu7kyWKHwwCgYIKoZIzj0EAwIDRwAw
-RAIgeSMNJ51+EvNJzqsISauhOTbFxiUnnmV2z/+dxYeCPzUCIEMXM/X2ekzHEz6V
-l7zSfosiYvtQQL3ML3sLnVMmxdmd
------END CERTIFICATE----- \ No newline at end of file
diff --git a/app/src/normalProductionFatDebug/assets/urls/demo.bitmask.net.url b/app/src/normalProductionFatDebug/assets/urls/demo.bitmask.net.url
deleted file mode 100644
index db80257f..00000000
--- a/app/src/normalProductionFatDebug/assets/urls/demo.bitmask.net.url
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "main_url" : "https://damo.bitmask.net",
- "geoip_url" : "https://menshen.demo.bitmask.net/json"
-}
diff --git a/app/src/production/java/se/leap/bitmaskclient/providersetup/ProviderApiManager.java b/app/src/production/java/se/leap/bitmaskclient/providersetup/ProviderApiManager.java
deleted file mode 100644
index 1f4e7e49..00000000
--- a/app/src/production/java/se/leap/bitmaskclient/providersetup/ProviderApiManager.java
+++ /dev/null
@@ -1,418 +0,0 @@
-/**
- * Copyright (c) 2018 LEAP Encryption Access Project and contributers
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
-package se.leap.bitmaskclient.providersetup;
-
-import static android.text.TextUtils.isEmpty;
-import static se.leap.bitmaskclient.BuildConfig.DEBUG_MODE;
-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.malformed_url;
-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.warning_corrupted_provider_cert;
-import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_details;
-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.models.Constants.PROVIDER_VPN_CERTIFICATE;
-import static se.leap.bitmaskclient.base.utils.ConfigHelper.getProviderFormattedString;
-import static se.leap.bitmaskclient.base.utils.BuildConfigHelper.isDefaultBitmask;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
-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.ProviderSetupObservable.DOWNLOADED_CA_CERT;
-import static se.leap.bitmaskclient.providersetup.ProviderSetupObservable.DOWNLOADED_EIP_SERVICE_JSON;
-import static se.leap.bitmaskclient.providersetup.ProviderSetupObservable.DOWNLOADED_PROVIDER_JSON;
-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.util.Pair;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.net.URL;
-import java.util.List;
-import java.util.concurrent.TimeoutException;
-
-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.utils.ConfigHelper;
-import se.leap.bitmaskclient.base.utils.PreferenceHelper;
-import se.leap.bitmaskclient.eip.EIP;
-import se.leap.bitmaskclient.providersetup.connectivity.OkHttpClientGenerator;
-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 ProviderApiManager extends ProviderApiManagerBase {
-
- private static final String TAG = ProviderApiManager.class.getSimpleName();
-
- public ProviderApiManager(Resources resources, OkHttpClientGenerator clientGenerator, ProviderApiServiceCallback callback) {
- super(resources, clientGenerator, callback);
- }
-
- /**
- * Only used in insecure flavor.
- */
- public static boolean lastDangerOn() {
- 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, 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.
- */
- @Override
- protected Bundle setUpProvider(Provider provider, Bundle task) {
- Bundle currentDownload = new Bundle();
-
- if (isEmpty(provider.getMainUrlString()) || provider.getMainUrl().isDefault()) {
- currentDownload.putBoolean(BROADCAST_RESULT_KEY, false);
- setErrorResult(currentDownload, malformed_url, null);
- VpnStatus.logWarning("[API] MainURL String is not set. Cannot setup provider.");
- return currentDownload;
- }
-
- getPersistedProviderUpdates(provider);
- 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);
- }
-
- currentDownload = getAndSetProviderJson(provider);
- if (provider.hasDefinition() || (currentDownload.containsKey(BROADCAST_RESULT_KEY) && currentDownload.getBoolean(BROADCAST_RESULT_KEY))) {
- ProviderSetupObservable.updateProgress(DOWNLOADED_PROVIDER_JSON);
- if (!provider.hasCaCert()) {
- currentDownload = downloadCACert(provider);
- }
- if (provider.hasCaCert() || (currentDownload.containsKey(BROADCAST_RESULT_KEY) && currentDownload.getBoolean(BROADCAST_RESULT_KEY))) {
- ProviderSetupObservable.updateProgress(DOWNLOADED_CA_CERT);
- currentDownload = getAndSetEipServiceJson(provider);
- }
-
- if (provider.hasEIP() && !provider.allowsRegistered() && !provider.allowsAnonymous()) {
- 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.getMainUrlString() + "/provider.json";
- providerDotJsonString = downloadWithCommercialCA(providerJsonUrl, provider);
- } else {
- providerDotJsonString = downloadFromApiUrlWithProviderCA("/provider.json", provider);
- }
-
- if (ConfigHelper.checkErroneousDownload(providerDotJsonString) || !isValidJson(providerDotJsonString)) {
- 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 setErrorResult(result, warning_corrupted_provider_details, ERROR_CORRUPTED_PROVIDER_JSON.toString());
- }
-
- } catch (JSONException e) {
- 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.
- */
- @Override
- protected 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)) {
- setErrorResult(result, eipServiceJsonString);
- } else {
- provider.setEipServiceJson(eipServiceJson);
- provider.setLastEipServiceUpdate(System.currentTimeMillis());
- result.putBoolean(BROADCAST_RESULT_KEY, true);
- }
- } catch (NullPointerException | JSONException e) {
- 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.
- */
- @Override
- 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()) {
- setErrorResult(result, downloading_vpn_certificate_failed, null);
- } else if (certString == null || certString.isEmpty() ){
- // probably 204
- setErrorResult(result, error_io_exception_user_message, null);
- } else {
- setErrorResult(result, certString);
- }
- return result;
- }
- return loadCertificate(provider, certString);
- }
-
- /**
- * 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
- */
- @Override
- protected Bundle getGeoIPJson(Provider provider) {
- Bundle result = new Bundle();
-
- if (!provider.shouldUpdateGeoIpJson() || provider.getGeoipUrl().isDefault() || VpnStatus.isVPNActive() || TorStatusObservable.getStatus() != OFF) {
- result.putBoolean(BROADCAST_RESULT_KEY, false);
- return result;
- }
-
- try {
- URL geoIpUrl = provider.getGeoipUrl().getUrl();
-
- 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 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 {
- 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 &&
- 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.getApiUrlString();
- 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 &&
- 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;
- }
-}
diff --git a/app/src/test/java/de/blinkt/openvpn/VpnProfileTest.java b/app/src/test/java/de/blinkt/openvpn/VpnProfileTest.java
index 43df146b..aa098b94 100644
--- a/app/src/test/java/de/blinkt/openvpn/VpnProfileTest.java
+++ b/app/src/test/java/de/blinkt/openvpn/VpnProfileTest.java
@@ -33,15 +33,16 @@ import se.leap.bitmaskclient.pluggableTransports.models.Obfs4Options;
public class VpnProfileTest {
private static final String OPENVPNCONNECTION_PROFILE = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mUseUdp\":false,\"mServerName\":\"openvpn.example.com\",\"mProxyType\":\"NONE\",\"mProxyPort\":\"8080\",\"mUseCustomConfig\":false,\"mConnectTimeout\":0,\"mProxyName\":\"proxy.example.com\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.OpenvpnConnection\",\"mServerPort\":\"1194\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":1,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}";
- private static final String OBFS4CONNECTION_PROFILE = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"127.0.0.1\",\"mProxyType\":\"NONE\",\"mConnectTimeout\":0,\"mServerPort\":\"8080\",\"mUseUdp\":true,\"mProxyPort\":\"\",\"mUseCustomConfig\":false,\"options\":{\"bridgeIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"portCount\":0,\"iatMode\":\"0\",\"cert\":\"CERT\",\"experimental\":false,\"portSeed\":0},\"type\":\"obfs4\",\"protocols\":[\"tcp\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":2,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}";
- private static final String OBFS4CONNECTION_PROFILE_OBFSVPN = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"127.0.0.1\",\"mProxyType\":\"NONE\",\"mConnectTimeout\":0,\"mServerPort\":\"8080\",\"mUseUdp\":true,\"mProxyPort\":\"\",\"mUseCustomConfig\":false,\"options\":{\"bridgeIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"portCount\":0,\"iatMode\":\"1\",\"cert\":\"CERT\",\"experimental\":false,\"portSeed\":0},\"type\":\"obfs4\",\"protocols\":[\"tcp\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":2,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}";
- private static final String OBFS4CONNECTION_PROFILE_OBFSVPN_KCP = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"127.0.0.1\",\"mProxyType\":\"NONE\",\"mConnectTimeout\":0,\"mServerPort\":\"8080\",\"mUseUdp\":true,\"mProxyPort\":\"\",\"mUseCustomConfig\":false,\"options\":{\"bridgeIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"portCount\":0,\"iatMode\":\"1\",\"cert\":\"CERT\",\"experimental\":false,\"portSeed\":0},\"type\":\"obfs4\",\"protocols\":[\"kcp\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":2,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}\n";
+ private static final String OBFS4CONNECTION_PROFILE = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"127.0.0.1\",\"mProxyType\":\"NONE\",\"mConnectTimeout\":0,\"mServerPort\":\"8080\",\"mUseUdp\":true,\"mProxyPort\":\"\",\"mUseCustomConfig\":false,\"options\":{\"bridgeIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"minHopSeconds\":0,\"portCount\":0,\"iatMode\":\"0\",\"cert\":\"CERT\",\"experimental\":false,\"minHopPort\":0,\"portSeed\":0,\"hopJitter\":0,\"maxHopPort\":0},\"type\":\"obfs4\",\"protocols\":[\"tcp\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":2,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}";
+ private static final String OBFS4CONNECTION_PROFILE_OBFSVPN = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"127.0.0.1\",\"mProxyType\":\"NONE\",\"mConnectTimeout\":0,\"mServerPort\":\"8080\",\"mUseUdp\":true,\"mProxyPort\":\"\",\"mUseCustomConfig\":false,\"options\":{\"bridgeIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"minHopSeconds\":0,\"portCount\":0,\"iatMode\":\"1\",\"cert\":\"CERT\",\"experimental\":false,\"minHopPort\":0,\"portSeed\":0,\"hopJitter\":0,\"maxHopPort\":0},\"type\":\"obfs4\",\"protocols\":[\"tcp\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":2,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}";
+ private static final String OBFS4CONNECTION_PROFILE_OBFSVPN_KCP = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"127.0.0.1\",\"mProxyType\":\"NONE\",\"mConnectTimeout\":0,\"mServerPort\":\"8080\",\"mUseUdp\":true,\"mProxyPort\":\"\",\"mUseCustomConfig\":false,\"options\":{\"bridgeIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"minHopSeconds\":0,\"portCount\":0,\"iatMode\":\"1\",\"cert\":\"CERT\",\"experimental\":false,\"minHopPort\":0,\"portSeed\":0,\"hopJitter\":0,\"maxHopPort\":0},\"type\":\"obfs4\",\"protocols\":[\"kcp\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":2,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}\n";
+ private static final String OBFS4CONNECTION_PROFILE_OBFSVPN_QUIC = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"127.0.0.1\",\"mProxyType\":\"NONE\",\"mConnectTimeout\":0,\"mServerPort\":\"8080\",\"mUseUdp\":true,\"mProxyPort\":\"\",\"mUseCustomConfig\":false,\"options\":{\"bridgeIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"minHopSeconds\":0,\"portCount\":0,\"iatMode\":\"1\",\"cert\":\"CERT\",\"experimental\":false,\"minHopPort\":0,\"portSeed\":0,\"hopJitter\":0,\"maxHopPort\":0},\"type\":\"obfs4\",\"protocols\":[\"quic\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":2,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}\n";
- private static final String OBFS4CONNECTION_PROFILE_OBFSVPN_HOP = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"127.0.0.1\",\"mProxyType\":\"NONE\",\"mConnectTimeout\":0,\"mServerPort\":\"8080\",\"mUseUdp\":true,\"mProxyPort\":\"\",\"mUseCustomConfig\":false,\"options\":{\"bridgeIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"portCount\":100,\"endpoints\":[{\"ip\":\"1.1.1.1\",\"cert\":\"CERT1\"},{\"ip\":\"2.2.2.2\",\"cert\":\"CERT2\"}],\"iatMode\":\"1\",\"experimental\":true,\"portSeed\":200},\"type\":\"obfs4\",\"protocols\":[\"tcp\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":3,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}\n";
+ private static final String OBFS4CONNECTION_PROFILE_OBFSVPN_HOP = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"127.0.0.1\",\"mProxyType\":\"NONE\",\"mConnectTimeout\":0,\"mServerPort\":\"8080\",\"mUseUdp\":true,\"mProxyPort\":\"\",\"mUseCustomConfig\":false,\"options\":{\"bridgeIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"minHopSeconds\":10,\"portCount\":100,\"endpoints\":[{\"ip\":\"1.1.1.1\",\"cert\":\"CERT1\"},{\"ip\":\"2.2.2.2\",\"cert\":\"CERT2\"}],\"iatMode\":\"1\",\"experimental\":true,\"minHopPort\":40000,\"portSeed\":2500,\"hopJitter\":10,\"maxHopPort\":64000},\"type\":\"obfs4\",\"protocols\":[\"tcp\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":3,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}\n";
- private static final String OBFS4CONNECTION_PROFILE_OBFSVPN_HOP_KCP = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"127.0.0.1\",\"mProxyType\":\"NONE\",\"mConnectTimeout\":0,\"mServerPort\":\"8080\",\"mUseUdp\":true,\"mProxyPort\":\"\",\"mUseCustomConfig\":false,\"options\":{\"bridgeIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"portCount\":100,\"endpoints\":[{\"ip\":\"1.1.1.1\",\"cert\":\"CERT1\"},{\"ip\":\"2.2.2.2\",\"cert\":\"CERT2\"}],\"iatMode\":\"1\",\"experimental\":true,\"portSeed\":2500},\"type\":\"obfs4\",\"protocols\":[\"kcp\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":3,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}\n";
+ private static final String OBFS4CONNECTION_PROFILE_OBFSVPN_HOP_KCP = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"127.0.0.1\",\"mProxyType\":\"NONE\",\"mConnectTimeout\":0,\"mServerPort\":\"8080\",\"mUseUdp\":true,\"mProxyPort\":\"\",\"mUseCustomConfig\":false,\"options\":{\"bridgeIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"minHopSeconds\":10,\"portCount\":100,\"endpoints\":[{\"ip\":\"1.1.1.1\",\"cert\":\"CERT1\"},{\"ip\":\"2.2.2.2\",\"cert\":\"CERT2\"}],\"iatMode\":\"1\",\"experimental\":true,\"minHopPort\":40000,\"portSeed\":2500,\"hopJitter\":10,\"maxHopPort\":64000},\"type\":\"obfs4\",\"protocols\":[\"kcp\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":3,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}\n";
- private static final String OBFS4CONNECTION_PROFILE_OBFSVPN_PORTHOPPING = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"127.0.0.1\",\"mProxyType\":\"NONE\",\"mConnectTimeout\":0,\"mServerPort\":\"8080\",\"mUseUdp\":true,\"mProxyPort\":\"\",\"mUseCustomConfig\":false,\"options\":{\"bridgeIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"portCount\":100,\"iatMode\":\"1\",\"cert\":\"CERT\",\"experimental\":true,\"portSeed\":200},\"type\":\"obfs4-hop\",\"protocols\":[\"tcp\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":3,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}\n";
+ private static final String OBFS4CONNECTION_PROFILE_OBFSVPN_PORTHOPPING = "{\"mCipher\":\"\",\"mProfileVersion\":7,\"mLastUsed\":0,\"mCheckRemoteCN\":true,\"mVerb\":\"1\",\"mRemoteRandom\":false,\"mRoutenopull\":false,\"mConnectRetry\":\"2\",\"mAllowedAppsVpn\":[],\"mUserEditable\":true,\"mUseUdp\":true,\"mAllowedAppsVpnAreDisallowed\":true,\"mDNS1\":\"8.8.8.8\",\"mDNS2\":\"8.8.4.4\",\"mUseCustomConfig\":false,\"mUseFloat\":false,\"mUseDefaultRoute\":true,\"mConnectRetryMaxTime\":\"300\",\"mNobind\":true,\"mVersion\":0,\"mConnectRetryMax\":\"-1\",\"mOverrideDNS\":false,\"mAuth\":\"\",\"mTunMtu\":0,\"mPassword\":\"\",\"mTLSAuthDirection\":\"\",\"mKeyPassword\":\"\",\"mCustomConfigOptions\":\"\",\"mName\":\"mockProfile\",\"mExpectTLSCert\":false,\"mUsername\":\"\",\"mAllowLocalLAN\":false,\"mDataCiphers\":\"\",\"mSearchDomain\":\"blinkt.de\",\"mTemporaryProfile\":false,\"mUseTLSAuth\":false,\"mRemoteCN\":\"\",\"mCustomRoutesv6\":\"\",\"mPersistTun\":false,\"mX509AuthType\":3,\"mUuid\":\"9d295ca2-3789-48dd-996e-f731dbf50fdc\",\"mServerName\":\"openvpn.example.com\",\"mMssFix\":0,\"mPushPeerInfo\":false,\"mAuthenticationType\":2,\"mBlockUnusedAddressFamilies\":true,\"mServerPort\":\"1194\",\"mUseDefaultRoutev6\":true,\"mConnections\":[{\"mCustomConfiguration\":\"\",\"mServerName\":\"127.0.0.1\",\"mProxyType\":\"NONE\",\"mConnectTimeout\":0,\"mServerPort\":\"8080\",\"mUseUdp\":true,\"mProxyPort\":\"\",\"mUseCustomConfig\":false,\"options\":{\"bridgeIP\":\"192.168.0.1\",\"transport\":{\"options\":{\"minHopSeconds\":10,\"portCount\":100,\"iatMode\":\"1\",\"cert\":\"CERT\",\"experimental\":true,\"minHopPort\":40000,\"portSeed\":2500,\"hopJitter\":10,\"maxHopPort\":64000},\"type\":\"obfs4-hop\",\"protocols\":[\"tcp\"],\"ports\":[\"1234\"]}},\"mProxyName\":\"\",\"mUseProxyAuth\":false,\"ConnectionAdapter.META_TYPE\":\"de.blinkt.openvpn.core.connection.Obfs4Connection\",\"mEnabled\":true}],\"mUseLzo\":false,\"mTransportType\":3,\"mAllowAppVpnBypass\":false,\"mUsePull\":true,\"mUseRandomHostname\":false,\"mAuthRetry\":0}\n";
@Before
public void setup() {
@@ -129,11 +130,29 @@ public class VpnProfileTest {
}
@Test
+ public void toJson_obfs4_obfsvpn_quic() throws JSONException {
+
+ VpnProfile mockVpnProfile = new VpnProfile("mockProfile", OBFS4);
+ Transport.Options options = new Transport.Options("CERT", "1");
+ Transport transport = new Transport(OBFS4.toString(), new String[]{"quic"}, new String[]{"1234"}, options);
+ mockVpnProfile.mConnections[0] = new Obfs4Connection(new Obfs4Options("192.168.0.1", transport));
+ mockVpnProfile.mLastUsed = 0;
+ String s = mockVpnProfile.toJson();
+
+ //ignore UUID in comparison -> set it to fixed value
+ JSONObject actual = new JSONObject(s);
+ actual.put("mUuid", "9d295ca2-3789-48dd-996e-f731dbf50fdc");
+ JSONObject expectation = new JSONObject(OBFS4CONNECTION_PROFILE_OBFSVPN_QUIC);
+
+ assertEquals(expectation.toString(),actual.toString());
+ }
+
+ @Test
public void toJson_obfs4hop_kcp() throws JSONException {
VpnProfile mockVpnProfile = new VpnProfile("mockProfile", OBFS4_HOP);
- Transport.Options options = new Transport.Options("1", new Transport.Endpoint[]{new Transport.Endpoint("1.1.1.1", "CERT1"), new Transport.Endpoint("2.2.2.2", "CERT2")}, 2500, 100, true);
+ Transport.Options options = new Transport.Options("1", new Transport.Endpoint[]{new Transport.Endpoint("1.1.1.1", "CERT1"), new Transport.Endpoint("2.2.2.2", "CERT2")}, 2500, 100, 40000, 64000, 10, 10, true);
Transport transport = new Transport(OBFS4.toString(), new String[]{"kcp"}, new String[]{"1234"}, options);
mockVpnProfile.mConnections[0] = new Obfs4Connection(new Obfs4Options("192.168.0.1", transport));
@@ -153,7 +172,7 @@ public class VpnProfileTest {
VpnProfile mockVpnProfile = new VpnProfile("mockProfile", OBFS4_HOP);
- Transport.Options options = new Transport.Options("1", null, "CERT",200, 100, true);
+ Transport.Options options = new Transport.Options("1", null, "CERT",2500, 100, 40000, 64000, 10, 10, true);
Transport transport = new Transport(OBFS4_HOP.toString(), new String[]{"tcp"}, new String[]{"1234"}, options);
mockVpnProfile.mConnections[0] = new Obfs4Connection(new Obfs4Options("192.168.0.1", transport));
@@ -172,7 +191,7 @@ public class VpnProfileTest {
public void toJson_obfs4hop() throws JSONException {
VpnProfile mockVpnProfile = new VpnProfile("mockProfile", OBFS4_HOP);
- Transport.Options options = new Transport.Options("1", new Transport.Endpoint[]{new Transport.Endpoint("1.1.1.1", "CERT1"), new Transport.Endpoint("2.2.2.2", "CERT2")}, 200, 100, true);
+ Transport.Options options = new Transport.Options("1", new Transport.Endpoint[]{new Transport.Endpoint("1.1.1.1", "CERT1"), new Transport.Endpoint("2.2.2.2", "CERT2")}, 2500, 100, 40000, 64000, 10, 10, true);
Transport transport = new Transport(OBFS4.toString(), new String[]{"tcp"}, new String[]{"1234"}, options);
mockVpnProfile.mConnections[0] = new Obfs4Connection(new Obfs4Options("192.168.0.1", transport));
@@ -242,4 +261,21 @@ public class VpnProfileTest {
assertEquals("192.168.0.1", obfs4Connection.getObfs4Options().bridgeIP);
assertEquals("1234", obfs4Connection.getObfs4Options().transport.getPorts()[0]);
}
+
+ @Test
+ public void fromJson_obfs4_obfsvpn_quic() {
+
+ VpnProfile mockVpnProfile = VpnProfile.fromJson(OBFS4CONNECTION_PROFILE_OBFSVPN_QUIC);
+ assertNotNull(mockVpnProfile);
+ assertNotNull(mockVpnProfile.mConnections);
+ assertNotNull(mockVpnProfile.mConnections[0]);
+ assertTrue(mockVpnProfile.mConnections[0].isUseUdp());
+ Obfs4Connection obfs4Connection = (Obfs4Connection) mockVpnProfile.mConnections[0];
+ assertEquals(OBFS4, obfs4Connection.getTransportType());
+ assertEquals("quic", obfs4Connection.getObfs4Options().transport.getProtocols()[0]);
+ assertEquals("CERT", obfs4Connection.getObfs4Options().transport.getOptions().getCert());
+ assertEquals("1", obfs4Connection.getObfs4Options().transport.getOptions().getIatMode());
+ assertEquals("192.168.0.1", obfs4Connection.getObfs4Options().bridgeIP);
+ assertEquals("1234", obfs4Connection.getObfs4Options().transport.getPorts()[0]);
+ }
} \ No newline at end of file
diff --git a/app/src/test/java/io/swagger/client/JSONTest.java b/app/src/test/java/io/swagger/client/JSONTest.java
new file mode 100644
index 00000000..5baa7e79
--- /dev/null
+++ b/app/src/test/java/io/swagger/client/JSONTest.java
@@ -0,0 +1,31 @@
+package io.swagger.client;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import static org.junit.Assert.*;
+
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
+
+import com.google.gson.JsonSyntaxException;
+
+import de.blinkt.openvpn.core.ConfigParser;
+import io.swagger.client.model.ModelsProvider;
+import se.leap.bitmaskclient.testutils.TestSetupHelper;
+
+public class JSONTest {
+
+ @Test
+ public void testProviderJsonParsing_testBackwardsCompatibility_v4() throws IOException {
+ String boblove = TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("v4/riseup.net.json"));
+ ModelsProvider p = JSON.createGson().create().fromJson(boblove, ModelsProvider.class);
+ assertNotNull(p);
+ assertEquals("riseup.net", p.getDomain());
+ }
+
+ @Test
+ public void testProvidingNull() throws IOException {
+ String p = JSON.createGson().create().toJson(null);
+ assertEquals("null", p);
+ }
+}
diff --git a/app/src/test/java/se/leap/bitmaskclient/base/models/GatewayJsonTest.java b/app/src/test/java/se/leap/bitmaskclient/base/models/GatewayJsonTest.java
index 801f98ad..1e07b293 100644
--- a/app/src/test/java/se/leap/bitmaskclient/base/models/GatewayJsonTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/base/models/GatewayJsonTest.java
@@ -12,7 +12,7 @@ public class GatewayJsonTest {
@Test
public void testToString() {
- String gatewayJSON = "{\"location\":\"Unknown Location\",\"ip_address\":\"1.2.3.4\",\"host\":\"pinned.obfuscation.proxy\",\"capabilities\":{\"adblock\":false,\"filter_dns\":false,\"limited\":false,\"transport\":[{\"type\":\"obfs4\",\"protocols\":[\"tcp\"],\"ports\":[\"1194\"],\"options\":{\"cert\":\"xxxxxxx\",\"iatMode\":\"0\",\"experimental\":false,\"portSeed\":0,\"portCount\":0}}],\"user_ips\":false}}";
+ String gatewayJSON = "{\"location\":\"Unknown Location\",\"ip_address\":\"1.2.3.4\",\"host\":\"pinned.obfuscation.proxy\",\"capabilities\":{\"adblock\":false,\"filter_dns\":false,\"limited\":false,\"transport\":[{\"type\":\"obfs4\",\"protocols\":[\"tcp\"],\"ports\":[\"1194\"],\"options\":{\"cert\":\"xxxxxxx\",\"iatMode\":\"0\",\"experimental\":false,\"portSeed\":0,\"portCount\":0,\"minHopPort\":0,\"maxHopPort\":0,\"minHopSeconds\":0,\"hopJitter\":0}}],\"user_ips\":false}}";
Connection.TransportType transportType = OBFS4;
Transport[] transports = new Transport[]{
diff --git a/app/src/test/java/se/leap/bitmaskclient/base/models/IntroducerTest.java b/app/src/test/java/se/leap/bitmaskclient/base/models/IntroducerTest.java
new file mode 100644
index 00000000..b53f06fe
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/base/models/IntroducerTest.java
@@ -0,0 +1,83 @@
+package se.leap.bitmaskclient.base.models;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import android.net.Uri;
+import android.os.Build;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+
+
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = {Build.VERSION_CODES.P})
+public class IntroducerTest {
+
+ @Test
+ public void testGetQueryParam() {
+ try {
+ String auth = "solitech_w4gOlm+abcdefaF2DE1Q6dg==";
+ String encodedAuth = URLEncoder.encode(auth, "UTF-8");
+ Uri uri = Uri.parse("obfsvpn://example.org:443?auth=" + encodedAuth);
+ assertEquals(auth, uri.getQueryParameter("auth"));
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ @Test
+ public void testFromUrl() {
+ try {
+ Introducer intro = Introducer.fromUrl("obfsvpnintro://37.2.240.90:443?fqdn=ft1.example.org&kcp=1&cert=XXXXXXX&auth=solitech_w4gOlm%2BsbF8spFL8E1Q6dg%3D%3D");
+ assertEquals(intro.getFullyQualifiedDomainName(), "ft1.example.org");
+ assertEquals("solitech_w4gOlm+sbF8spFL8E1Q6dg==", intro.getAuthToken());
+ assertEquals("obfsvpnintro://37.2.240.90:443?fqdn=ft1.example.org&kcp=1&cert=XXXXXXX&auth=solitech_w4gOlm%2BsbF8spFL8E1Q6dg%3D%3D", intro.toUrl());
+ } catch (URISyntaxException | UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ public void testFromUrl_homograph_attack() {
+ String code = "obfsvpnintro://37.2.240.90:443?fqdn=ft1.bitmasк.net&kcp=0&cert=XXXXXXX&auth=solitech_w4gOlm%2BseC5spDL8E1Q6dg";
+ assertThrows(IllegalArgumentException.class, () -> Introducer.fromUrl(code));
+ }
+
+ @Test
+ public void testFromUrl_invalid_fqdn() {
+ String code = "obfsvpnintro://37.2.240.90:443?fqdn=file://var/wwww&kcp=0&cert=XXXXXXX&auth=solitech_w4gOlm%2BseC5spDL8E1Q6dg";
+ assertThrows(IllegalArgumentException.class, () -> Introducer.fromUrl(code));
+ }
+
+ @Test
+ public void testFromUrl_missing_fqdn() {
+ String code = "obfsvpnintro://37.2.240.90:443?fqdn=&kcp=0&cert=XXXXXXX&auth=solitech_w4gOlm%2BseC5spDL8E1Q6dg";
+ assertThrows(IllegalArgumentException.class, () -> Introducer.fromUrl(code));
+
+ String code2 = "obfsvpnintro://37.2.240.90:443?kcp=0&cert=XXXXXXX&auth=solitech_w4gOlm%2BseC5spDL8E1Q6dg";
+ assertThrows(IllegalArgumentException.class, () -> Introducer.fromUrl(code2));
+ }
+
+ @Test
+ public void testFromUrl_missing_cert() {
+ String code = "obfsvpnintro://37.2.240.90:443?fqdn=ft1.bitmask.net&kcp=0&cert=&auth=solitech_w4gOlm%2BseC5spDL8E1Q6dg";
+ assertThrows(IllegalArgumentException.class, () -> Introducer.fromUrl(code));
+ }
+
+ @Test
+ public void testFromUrl_missing_auth() {
+ String code = "obfsvpnintro://37.2.240.90:443?fqdn=ft1.bitmask.net&kcp=0&cert=XXXXXXX&auth=";
+ assertThrows(IllegalArgumentException.class, () -> Introducer.fromUrl(code));
+
+ String code2 = "obfsvpnintro://37.2.240.90:443?fqdn=ft1.bitmask.net&kcp=0&cert=XXXXXXX";
+ assertThrows(IllegalArgumentException.class, () -> Introducer.fromUrl(code2));
+ }
+}
diff --git a/app/src/test/java/se/leap/bitmaskclient/base/models/ProviderTest.java b/app/src/test/java/se/leap/bitmaskclient/base/models/ProviderTest.java
index ee6cd30f..fc2e6d03 100644
--- a/app/src/test/java/se/leap/bitmaskclient/base/models/ProviderTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/base/models/ProviderTest.java
@@ -5,8 +5,13 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
+import android.os.Build;
+
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
import se.leap.bitmaskclient.base.utils.BuildConfigHelper;
import se.leap.bitmaskclient.testutils.MockHelper;
@@ -16,6 +21,8 @@ import se.leap.bitmaskclient.testutils.TestSetupHelper;
* Created by cyberta on 12.02.18.
*/
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = {Build.VERSION_CODES.P})
public class ProviderTest {
@Before
@@ -118,6 +125,20 @@ public class ProviderTest {
}
@Test
+ public void testIsExperimentalPluggableTransportsSupported_Obfs4Quic_returnsTrue() throws Exception {
+ Provider p1 = TestSetupHelper.getProvider(
+ "https://pt.demo.bitmask.net",
+ null,
+ null,
+ null,
+ null,
+ null,
+ "multiple_pts_per_host_eip-service.json",
+ null);
+ assertTrue(p1.supportsExperimentalPluggableTransports());
+ }
+
+ @Test
public void testSupportsPluggableTransports_Obfs4Kcp_obsvpn_returnsTrue() throws Exception {
BuildConfigHelper helper = MockHelper.mockBuildConfigHelper(true);
@@ -133,4 +154,20 @@ public class ProviderTest {
assertTrue(p1.supportsPluggableTransports());
}
+ @Test
+ public void testSupportsPluggableTransports_Obfs4Quic_obsvpn_returnsTrue() throws Exception {
+ BuildConfigHelper helper = MockHelper.mockBuildConfigHelper(true);
+
+ Provider p1 = TestSetupHelper.getProvider(
+ "https://pt.demo.bitmask.net",
+ null,
+ null,
+ null,
+ null,
+ null,
+ "ptdemo_only_experimental_transports_gateways.json",
+ null);
+ assertTrue(p1.supportsPluggableTransports());
+ }
+
}
diff --git a/app/src/test/java/se/leap/bitmaskclient/base/utils/ConfigHelperTest.java b/app/src/test/java/se/leap/bitmaskclient/base/utils/ConfigHelperTest.java
index 6e0ceb56..fd862f2b 100644
--- a/app/src/test/java/se/leap/bitmaskclient/base/utils/ConfigHelperTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/base/utils/ConfigHelperTest.java
@@ -1,6 +1,8 @@
package se.leap.bitmaskclient.base.utils;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
@@ -11,6 +13,12 @@ import org.junit.runner.RunWith;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.modules.junit4.PowerMockRunnerDelegate;
+import java.io.IOException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+
+import se.leap.bitmaskclient.testutils.TestSetupHelper;
+
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(DataProviderRunner.class)
public class ConfigHelperTest {
@@ -57,4 +65,27 @@ public class ConfigHelperTest {
assertEquals("domain.co.uk", ConfigHelper.getDomainFromMainURL("https://subdomain.domain.co.uk"));
assertEquals("domain.co.uk", ConfigHelper.getDomainFromMainURL("https://domain.co.uk"));
}
+
+ @Test
+ public void testGetDomainFromMainURL_returnNullIfInvalid() {
+ assertNull(ConfigHelper.getDomainFromMainURL("https://localhost"));
+ assertNull(ConfigHelper.getDomainFromMainURL("http://localhost"));
+ assertNull(ConfigHelper.getDomainFromMainURL("invalidrandomstring"));
+ assertNull(ConfigHelper.getDomainFromMainURL(null));
+ }
+
+ @Test
+ public void testParseX509CertificatesFromString() throws IOException {
+ ArrayList<X509Certificate> certs = ConfigHelper.parseX509CertificatesFromString(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("updated_cert.pem")));
+ assertTrue(certs != null);
+ }
+
+ @Test
+ public void testParseX509CertificatesToString() throws IOException {
+ String certsString = TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("updated_cert.pem"));
+ ArrayList<X509Certificate> certs = ConfigHelper.parseX509CertificatesFromString(certsString);
+ String parsedCerts = ConfigHelper.parseX509CertificatesToString(certs);
+ assertEquals(certsString, parsedCerts);
+ }
+
} \ No newline at end of file
diff --git a/app/src/test/java/se/leap/bitmaskclient/base/utils/CredentialsParserTest.java b/app/src/test/java/se/leap/bitmaskclient/base/utils/CredentialsParserTest.java
new file mode 100644
index 00000000..55a9e94f
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/base/utils/CredentialsParserTest.java
@@ -0,0 +1,57 @@
+package se.leap.bitmaskclient.base.utils;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Build;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.testutils.TestSetupHelper;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = {Build.VERSION_CODES.LOLLIPOP, Build.VERSION_CODES.P})
+public class CredentialsParserTest {
+
+ @Test
+ public void testCertificateResponse() throws IOException, XmlPullParserException {
+ String ed25519_creds = TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ed25519_credentials.pem"));
+ Provider provider = new Provider("https://demo.bitmask.net");
+ CredentialsParser.parseXml(ed25519_creds, provider);
+ assertEquals(
+ "-----BEGIN PRIVATE KEY-----\n" +
+ "MC4CAQAwBQYDK2VwBCIEIF+HZvpSdhnTbYeT635bT2+IU4FbW3EWlHuUnXvhb10m\n" +
+ "-----END PRIVATE KEY-----", provider.getPrivateKeyString());
+ assertEquals(
+ "-----BEGIN CERTIFICATE-----\n" +
+ "MIIBgzCCASigAwIBAgIRALD3Z4SsobpcU7tcC0r9JOQwCgYIKoZIzj0EAwIwNzE1\n" +
+ "MDMGA1UEAwwsUHJvdmlkZXIgUm9vdCBDQSAoY2xpZW50IGNlcnRpZmljYXRlcyBv\n" +
+ "bmx5ISkwHhcNMjQxMTA1MTU0MjU0WhcNMjQxMTI5MTU0MjU0WjAUMRIwEAYDVQQD\n" +
+ "EwlVTkxJTUlURUQwKjAFBgMrZXADIQC5QkZAcpkQ3Rm54gN5iLEU1Zp1w+patXVT\n" +
+ "W9GRXmFz+6NnMGUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMC\n" +
+ "MB0GA1UdDgQWBBRMxeMW4vqGK7FBkDt2+8upfkK1kzAfBgNVHSMEGDAWgBS0pVQs\n" +
+ "1wnvNYG0AnmkxUcLOw+BLDAKBggqhkjOPQQDAgNJADBGAiEAg112+zWMm9qrPTvK\n" +
+ "99IMa+wbeNzZLSoN9xewf5rxOX0CIQCvMi08JcajsAJ9Dg6YAQgpmFdb35HDCzve\n" +
+ "lhkTCWJpgQ==\n" +
+ "-----END CERTIFICATE-----", provider.getVpnCertificate());
+ assertEquals(
+ "-----BEGIN CERTIFICATE-----\n" +
+ "MIIBozCCAUigAwIBAgIBATAKBggqhkjOPQQDAjA3MTUwMwYDVQQDDCxQcm92aWRl\n" +
+ "ciBSb290IENBIChjbGllbnQgY2VydGlmaWNhdGVzIG9ubHkhKTAeFw0yNDEwMjMx\n" +
+ "MjA0MjRaFw0yOTEwMjMxMjA5MjRaMDcxNTAzBgNVBAMMLFByb3ZpZGVyIFJvb3Qg\n" +
+ "Q0EgKGNsaWVudCBjZXJ0aWZpY2F0ZXMgb25seSEpMFkwEwYHKoZIzj0CAQYIKoZI\n" +
+ "zj0DAQcDQgAEMImwbNTDrXMeWfyTb2TMNzXNr79OsKjLDdZWqVT0iHMI8apo2P4H\n" +
+ "eXCHVGjS2Z+jpyI1u9ic3igThsKEmdZMSKNFMEMwDgYDVR0PAQH/BAQDAgKkMBIG\n" +
+ "A1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFLSlVCzXCe81gbQCeaTFRws7D4Es\n" +
+ "MAoGCCqGSM49BAMCA0kAMEYCIQCw88nXg/vs/KgGqH1uPs9oZkOxucVn/ZEznYzg\n" +
+ "szLhtAIhAPY32oHwmj3yHO9H2Jp7x0CoHuu1fKd9fQTBvEEbi7o9\n" +
+ "-----END CERTIFICATE-----", provider.getCaCert());
+ }
+
+}
diff --git a/app/src/test/java/se/leap/bitmaskclient/base/utils/PreferenceHelperTest.java b/app/src/test/java/se/leap/bitmaskclient/base/utils/PreferenceHelperTest.java
index e03fccff..1b093f62 100644
--- a/app/src/test/java/se/leap/bitmaskclient/base/utils/PreferenceHelperTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/base/utils/PreferenceHelperTest.java
@@ -1,5 +1,13 @@
package se.leap.bitmaskclient.base.utils;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_EIP_DEFINITION;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PRIVATE_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getSavedProviderFromSharedPreferences;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.getInputAsString;
+
import android.content.SharedPreferences;
import org.junit.Before;
@@ -8,15 +16,6 @@ import org.junit.Test;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.testutils.MockSharedPreferences;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_EIP_DEFINITION;
-import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PRIVATE_KEY;
-import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.preferUDP;
-import static se.leap.bitmaskclient.testutils.TestSetupHelper.getInputAsString;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getSavedProviderFromSharedPreferences;
-
/**
* Created by cyberta on 17.01.18.
*/
diff --git a/app/src/test/java/se/leap/bitmaskclient/base/utils/PrivateKeyHelperTest.java b/app/src/test/java/se/leap/bitmaskclient/base/utils/PrivateKeyHelperTest.java
new file mode 100644
index 00000000..5b1d4554
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/base/utils/PrivateKeyHelperTest.java
@@ -0,0 +1,48 @@
+package se.leap.bitmaskclient.base.utils;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Build;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.io.IOException;
+import java.security.PrivateKey;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.EdECPrivateKey;
+import java.security.interfaces.RSAPrivateKey;
+
+import se.leap.bitmaskclient.testutils.TestSetupHelper;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = {Build.VERSION_CODES.UPSIDE_DOWN_CAKE, Build.VERSION_CODES.P, Build.VERSION_CODES.O, Build.VERSION_CODES.N})
+public class PrivateKeyHelperTest {
+
+ @Test
+ public void parsePrivateKeyFromString_testRSA() throws IOException {
+ String rsa_key = TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("private_rsa_key.pem"));
+ PrivateKey pk = PrivateKeyHelper.parsePrivateKeyFromString(rsa_key);
+ assertNotNull(pk);
+ assertTrue(pk instanceof RSAPrivateKey);
+ }
+
+ @Test
+ public void parsePrivateKeyFromString_testEd25519() throws IOException {
+ String ed25519_key = TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("private_PKCS8_encoded_ed25519_key.pem"));
+ PrivateKey pk = PrivateKeyHelper.parsePrivateKeyFromString(ed25519_key);
+ assertNotNull(pk);
+ assertTrue(pk instanceof EdECPrivateKey);
+ }
+
+ @Test
+ public void parsePrivateKeyFromString_testEcDSA() throws IOException {
+ String ed_dsa_key = TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("private_PKCS8_encoded_ecdsa_key.pem"));
+ PrivateKey pk = PrivateKeyHelper.parsePrivateKeyFromString(ed_dsa_key);
+ assertNotNull(pk);
+ assertTrue(pk instanceof ECPrivateKey);
+ }
+} \ No newline at end of file
diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/GatewayTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/GatewayTest.java
new file mode 100644
index 00000000..fc062079
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/eip/GatewayTest.java
@@ -0,0 +1,159 @@
+package se.leap.bitmaskclient.eip;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static se.leap.bitmaskclient.base.models.Constants.GATEWAYS;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.getProvider;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+
+import androidx.annotation.Nullable;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+
+import de.blinkt.openvpn.VpnProfile;
+import de.blinkt.openvpn.core.ConfigParser;
+import de.blinkt.openvpn.core.connection.Connection;
+import de.blinkt.openvpn.core.connection.Obfs4Connection;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import se.leap.bitmaskclient.testutils.MockSharedPreferences;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = {Build.VERSION_CODES.P})
+public class GatewayTest {
+
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private Context mockContext;
+
+ private SharedPreferences sharedPreferences;
+
+ @Before
+ public void setUp() throws IOException, JSONException {
+ sharedPreferences = new MockSharedPreferences();
+
+ PreferenceHelper preferenceHelper = new PreferenceHelper(sharedPreferences);
+ }
+
+
+ private Gateway createGatewayFromProvider(int nClosest, @Nullable String eipServiceJsonPath) throws ConfigParser.ConfigParseError, JSONException, IOException {
+ Provider provider = getProvider(null, null, null, null, null, null, eipServiceJsonPath, null);
+ JSONObject eipServiceJson = provider.getEipServiceJson();
+
+ JSONObject gatewayJson = eipServiceJson.getJSONArray(GATEWAYS).getJSONObject(0);
+ JSONObject secrets = new JSONObject();
+ try {
+ secrets.put(Provider.CA_CERT, provider.getCaCert());
+ secrets.put(PROVIDER_VPN_CERTIFICATE, provider.getVpnCertificate());
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+
+ return new Gateway(eipServiceJson, secrets, gatewayJson);
+ }
+ @Test
+ public void testGetProfile_OpenVPN_obfuscationProtocols_ignored_OpenVPNfound() throws ConfigParser.ConfigParseError, JSONException, IOException {
+ Gateway gateway = createGatewayFromProvider(0, "ptdemo_three_mixed_gateways.json");
+ VpnProfile profile = gateway.getProfile(Connection.TransportType.OPENVPN, new HashSet<>(Arrays.asList("invalid")));
+ assertNotNull(profile);
+ }
+
+ @Test
+ public void testGetProfile_obfs4_obfuscationProtocolsTakenIntoAccount_Obfs4Notfound() throws ConfigParser.ConfigParseError, JSONException, IOException {
+ Gateway gateway = createGatewayFromProvider(0, "ptdemo_three_mixed_gateways.json");
+ VpnProfile profile = gateway.getProfile(Connection.TransportType.OBFS4, new HashSet<>(Arrays.asList("invalid")));
+ assertNull(profile);
+ }
+
+ @Test
+ public void testGetProfile_obfs4_obfuscationProtocolsTakenIntoAccount_Obfs4found() throws ConfigParser.ConfigParseError, JSONException, IOException {
+ Gateway gateway = createGatewayFromProvider(0, "ptdemo_three_mixed_gateways.json");
+ VpnProfile profile = gateway.getProfile(Connection.TransportType.OBFS4, new HashSet<>(Arrays.asList("tcp")));
+ assertNotNull(profile);
+ }
+
+ @Test
+ public void testGetProfile_obfs4_obfuscationProtocolsTakenIntoAccount_Obfs4KCPfound() throws ConfigParser.ConfigParseError, JSONException, IOException {
+ Gateway gateway = createGatewayFromProvider(0, "multiple_pts_per_host_eip-service.json");
+ VpnProfile profile = gateway.getProfile(Connection.TransportType.OBFS4, new HashSet<>(Arrays.asList("kcp")));
+ assertNotNull(profile);
+ }
+
+ @Test
+ public void testGetProfile_obfs4_multipleProfiles_randomlySelected() throws ConfigParser.ConfigParseError, JSONException, IOException {
+ Gateway gateway = createGatewayFromProvider(0, "multiple_pts_per_host_eip-service.json");
+ VpnProfile profile1 = gateway.getProfile(Connection.TransportType.OBFS4, new HashSet<>(Arrays.asList("kcp", "tcp")));
+ assertNotNull(profile1);
+ assertEquals(1, profile1.mConnections.length);
+ assertTrue(profile1.mConnections[0] instanceof Obfs4Connection);
+ String[] transportLayerProtocols = ((Obfs4Connection)profile1.mConnections[0]).getObfs4Options().transport.getProtocols();
+
+ boolean profileWithDifferentTransportLayerProtosFound = false;
+ for (int i = 0; i < 1000; i++) {
+ VpnProfile otherProfile = gateway.getProfile(Connection.TransportType.OBFS4, new HashSet<>(Arrays.asList("kcp", "tcp")));
+ String[] otherProtocols = ((Obfs4Connection)otherProfile.mConnections[0]).getObfs4Options().transport.getProtocols();
+ if (!transportLayerProtocols[0].equals(otherProtocols[0])) {
+ profileWithDifferentTransportLayerProtosFound = true;
+ System.out.println(i + 1 + " attempts");
+ break;
+ }
+ }
+ assertTrue(profileWithDifferentTransportLayerProtosFound);
+ }
+
+ @Test
+ public void testSupportsTransport() throws ConfigParser.ConfigParseError, JSONException, IOException {
+ Gateway gateway = createGatewayFromProvider(0, "multiple_pts_per_host_eip-service.json");
+ assertFalse(gateway.supportsTransport(Connection.TransportType.OBFS4_HOP, null));
+ assertTrue(gateway.supportsTransport(Connection.TransportType.OBFS4, null));
+ assertTrue(gateway.supportsTransport(Connection.TransportType.OBFS4, new HashSet<>(Arrays.asList("kcp"))));
+ assertTrue(gateway.supportsTransport(Connection.TransportType.OBFS4, new HashSet<>(Arrays.asList("tcp"))));
+ assertTrue(gateway.supportsTransport(Connection.TransportType.OBFS4, new HashSet<>(Arrays.asList("quic"))));
+ assertFalse(gateway.supportsTransport(Connection.TransportType.OBFS4, new HashSet<>(Arrays.asList("invalid"))));
+ }
+
+ @Test
+ public void testGetSupportedTransports() throws ConfigParser.ConfigParseError, JSONException, IOException {
+ Gateway gateway = createGatewayFromProvider(0, "multiple_pts_per_host_eip-service.json");
+ assertEquals(2, gateway.getSupportedTransports().size());
+ assertTrue(gateway.getSupportedTransports().contains(Connection.TransportType.OBFS4));
+ assertTrue(gateway.getSupportedTransports().contains(Connection.TransportType.OPENVPN));
+ }
+
+ @Test
+ public void testHasProfile() throws ConfigParser.ConfigParseError, JSONException, IOException {
+ Gateway gateway = createGatewayFromProvider(0, "multiple_pts_per_host_eip-service.json");
+ VpnProfile profile = gateway.getProfiles().get(0);
+ String profileString = profile.toJson();
+ VpnProfile newProfile = VpnProfile.fromJson(profileString);
+ assertTrue(gateway.hasProfile(newProfile));
+
+ newProfile.mGatewayIp = "XXXX";
+ assertFalse(gateway.hasProfile(newProfile));
+
+ VpnProfile newProfile2 = VpnProfile.fromJson(profileString);
+ newProfile2.mConnections = new Connection[0];
+ assertFalse(gateway.hasProfile(newProfile2));
+ }
+
+
+} \ No newline at end of file
diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
index 9286a787..a9a73628 100644
--- a/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
@@ -3,6 +3,7 @@ package se.leap.bitmaskclient.eip;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNull;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
@@ -19,14 +20,19 @@ import static se.leap.bitmaskclient.testutils.TestSetupHelper.getProvider;
import android.content.Context;
import android.content.SharedPreferences;
+import android.os.Build;
+
+import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
-import org.junit.function.ThrowingRunnable;
+import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.Mock;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
import java.io.IOException;
import java.util.ArrayList;
@@ -34,9 +40,11 @@ import java.util.List;
import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.ConfigParser;
+import de.blinkt.openvpn.core.connection.Connection;
import se.leap.bitmaskclient.base.models.Location;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.base.models.ProviderObservable;
+import se.leap.bitmaskclient.base.models.Transport;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
import se.leap.bitmaskclient.base.utils.TimezoneHelper;
import se.leap.bitmaskclient.testutils.MockSharedPreferences;
@@ -45,6 +53,8 @@ import se.leap.bitmaskclient.testutils.TestSetupHelper;
/**
* Created by cyberta on 09.10.17.
*/
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = {Build.VERSION_CODES.P})
public class GatewaysManagerTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
@@ -87,6 +97,18 @@ public class GatewaysManagerTest {
assertEquals(3, gatewaysManager.size());
}
+ @Nullable
+ private static VpnProfile createProfile(VpnConfigGenerator configGenerator, Connection.TransportType transportType) throws IOException, ConfigParser.ConfigParseError, JSONException {
+ VpnProfile profile = null;
+ for (Transport transport : configGenerator.transports) {
+ if (transport.getTransportType() == transportType) {
+ profile = configGenerator.createProfile(transport);
+ break;
+ }
+ }
+ return profile;
+ }
+
@Test
public void TestGetPosition_VpnProfileExtistingObfs4_returnPositionZero() throws JSONException, ConfigParser.ConfigParseError, IOException {
Provider provider = getProvider(null, null, null, null, null, null, "ptdemo_three_mixed_gateways.json", null);
@@ -98,10 +120,12 @@ public class GatewaysManagerTest {
VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
configuration.apiVersion = 3;
configuration.remoteGatewayIP = "37.218.247.60";
- VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, gateway1, configuration);
- VpnProfile profile = configGenerator.createProfile(OBFS4);
-
+ configuration.transports = Transport.createTransportsFrom(gateway1, 3);
+ VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, configuration);
+ VpnProfile profile = createProfile(configGenerator, OBFS4);
+ assertNotNull(profile);
assertEquals(0, gatewaysManager.getPosition(profile));
+
}
@Test
@@ -115,9 +139,10 @@ public class GatewaysManagerTest {
VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
configuration.apiVersion = 3;
configuration.remoteGatewayIP = "37.218.247.60";
- VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, gateway1, configuration);
- VpnProfile profile = configGenerator.createProfile(OPENVPN);
-
+ configuration.transports = Transport.createTransportsFrom(gateway1, 3);
+ VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, configuration);
+ VpnProfile profile = createProfile(configGenerator, OPENVPN);
+ assertNotNull(profile);
assertEquals(0, gatewaysManager.getPosition(profile));
}
@@ -132,8 +157,9 @@ public class GatewaysManagerTest {
VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
configuration.apiVersion = 3;
configuration.remoteGatewayIP = "37.218.247.60";
- VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, gateway1, configuration);
- assertThrows(ConfigParser.ConfigParseError.class, () -> configGenerator.createProfile(OBFS4));
+ configuration.transports = Transport.createTransportsFrom(gateway1, 3);
+ VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, configuration);
+ assertThrows(ConfigParser.ConfigParseError.class, () -> createProfile(configGenerator, OBFS4));
}
@Test
@@ -147,10 +173,11 @@ public class GatewaysManagerTest {
VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
configuration.apiVersion = 3;
configuration.remoteGatewayIP = "37.218.247.60";
- VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, gateway1, configuration);
- VpnProfile profile = configGenerator.createProfile(OBFS4);
+ configuration.transports = Transport.createTransportsFrom(gateway1, 3);
+ VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, configuration);
+ VpnProfile profile = createProfile(configGenerator, OBFS4);
- assertEquals(1, gatewaysManager.getPosition(profile));
+ assertEquals(2, gatewaysManager.getPosition(profile));
}
@Test
@@ -164,8 +191,9 @@ public class GatewaysManagerTest {
VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
configuration.apiVersion = 3;
configuration.remoteGatewayIP = "37.218.247.60";
- VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, gateway1, configuration);
- VpnProfile profile = configGenerator.createProfile(OPENVPN);
+ configuration.transports = Transport.createTransportsFrom(gateway1, 3);
+ VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, configuration);
+ VpnProfile profile = createProfile(configGenerator, OPENVPN);
assertEquals(2, gatewaysManager.getPosition(profile));
}
@@ -181,8 +209,9 @@ public class GatewaysManagerTest {
VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
configuration.apiVersion = 3;
configuration.remoteGatewayIP = "37.218.247.61";
- VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, gateway1, configuration);
- VpnProfile profile = configGenerator.createProfile(OBFS4);
+ configuration.transports = Transport.createTransportsFrom(gateway1, 3);
+ VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, configuration);
+ VpnProfile profile = createProfile(configGenerator, OBFS4);
assertEquals(-1, gatewaysManager.getPosition(profile));
}
@@ -198,8 +227,9 @@ public class GatewaysManagerTest {
VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
configuration.apiVersion = 3;
configuration.remoteGatewayIP = "3.21.247.89";
- VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, gateway1, configuration);
- VpnProfile profile = configGenerator.createProfile(OBFS4);
+ configuration.transports = Transport.createTransportsFrom(gateway1, 3);
+ VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, configuration);
+ VpnProfile profile = createProfile(configGenerator, OBFS4);
assertEquals(1, gatewaysManager.getPosition(profile));
}
@@ -212,7 +242,7 @@ public class GatewaysManagerTest {
sharedPreferences.edit().putBoolean(USE_BRIDGES, true).commit();
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
- assertEquals("37.12.247.10", gatewaysManager.select(0).gateway.getRemoteIP());
+ assertEquals("37.12.247.10", gatewaysManager.selectVpnProfile(0).mGatewayIp);
}
@Test
@@ -226,11 +256,11 @@ public class GatewaysManagerTest {
commit();
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
ArrayList<String> hosts = new ArrayList<>();
- hosts.add(gatewaysManager.select(0).gateway.getHost());
- hosts.add(gatewaysManager.select(1).gateway.getHost());
+ hosts.add(gatewaysManager.selectVpnProfile(0).mGatewayIp);
+ hosts.add(gatewaysManager.selectVpnProfile(1).mGatewayIp);
- assertTrue(hosts.contains("bridge-nyc1-02.bitmask-dev.leapvpn.net"));
- assertTrue(hosts.contains("bridge-nyc1-01.bitmask-dev.leapvpn.net"));
+ assertTrue(hosts.contains("192.81.208.164"));
+ assertTrue(hosts.contains("104.248.232.240"));
}
@@ -246,10 +276,10 @@ public class GatewaysManagerTest {
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
ArrayList<String> hosts = new ArrayList<>();
- hosts.add(gatewaysManager.select(0).gateway.getHost());
- hosts.add(gatewaysManager.select(1).gateway.getHost());
- assertTrue(hosts.contains("bridge-nyc1-02.bitmask-dev.leapvpn.net"));
- assertTrue(hosts.contains("bridge-nyc1-01.bitmask-dev.leapvpn.net"));
+ hosts.add(gatewaysManager.selectVpnProfile(0).mGatewayIp);
+ hosts.add(gatewaysManager.selectVpnProfile(1).mGatewayIp);
+ assertTrue(hosts.contains("192.81.208.164"));
+ assertTrue(hosts.contains("104.248.232.240"));
}
@Test
@@ -264,7 +294,7 @@ public class GatewaysManagerTest {
for (int i = 0; i < 1000; i++) {
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
- assertEquals("bridge-nyc1-01.bitmask-dev.leapvpn.net", gatewaysManager.select(0).gateway.getHost());
+ assertEquals("104.248.232.240", gatewaysManager.selectVpnProfile(0).mGatewayIp);
}
}
@@ -279,8 +309,8 @@ public class GatewaysManagerTest {
commit();
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
- assertEquals("bridge-nyc1-01.bitmask-dev.leapvpn.net", gatewaysManager.select(0).gateway.getHost());
- assertNull(gatewaysManager.select(1));
+ assertEquals("104.248.232.240", gatewaysManager.selectVpnProfile(0).mGatewayIp);
+ assertNull(gatewaysManager.selectVpnProfile(1));
}
@Test
@@ -295,11 +325,11 @@ public class GatewaysManagerTest {
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
ArrayList<String> hosts = new ArrayList<>();
- hosts.add(gatewaysManager.select(0).gateway.getHost());
- hosts.add(gatewaysManager.select(1).gateway.getHost());
+ hosts.add(gatewaysManager.selectVpnProfile(0).mGatewayIp);
+ hosts.add(gatewaysManager.selectVpnProfile(1).mGatewayIp);
- assertTrue(hosts.contains("pt.demo.bitmask.net"));
- assertTrue(hosts.contains("manila.bitmask.net"));
+ assertTrue(hosts.contains("37.218.247.60"));
+ assertTrue(hosts.contains("37.12.247.10"));
}
@Test
@@ -309,9 +339,9 @@ public class GatewaysManagerTest {
providerObservable.updateProvider(provider);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
- assertEquals("manila.bitmask.net", gatewaysManager.select(0).gateway.getHost());
- assertEquals("moscow.bitmask.net", gatewaysManager.select(1).gateway.getHost());
- assertEquals("pt.demo.bitmask.net", gatewaysManager.select(2).gateway.getHost());
+ assertEquals("37.12.247.10", gatewaysManager.selectVpnProfile(0).mGatewayIp);
+ assertEquals("3.21.247.89", gatewaysManager.selectVpnProfile(1).mGatewayIp);
+ assertEquals("37.218.247.60", gatewaysManager.selectVpnProfile(2).mGatewayIp);
}
@Test
@@ -325,9 +355,9 @@ public class GatewaysManagerTest {
commit();
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
- assertEquals("moscow.bitmask.net", gatewaysManager.select(0).gateway.getHost());
- assertEquals("pt.demo.bitmask.net", gatewaysManager.select(1).gateway.getHost());
- assertNull(gatewaysManager.select(2));
+ assertEquals("3.21.247.89", gatewaysManager.selectVpnProfile(0).mGatewayIp);
+ assertEquals("37.218.247.60", gatewaysManager.selectVpnProfile(1).mGatewayIp);
+ assertNull(gatewaysManager.selectVpnProfile(2));
}
@@ -342,9 +372,9 @@ public class GatewaysManagerTest {
commit();
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
- assertEquals("mouette.riseup.net", gatewaysManager.select(0).gateway.getHost());
- assertEquals("hoatzin.riseup.net", gatewaysManager.select(1).gateway.getHost());
- assertEquals("zarapito.riseup.net", gatewaysManager.select(2).gateway.getHost());
+ assertEquals("163.172.126.44", gatewaysManager.selectVpnProfile(0).mGatewayIp);
+ assertEquals("212.83.143.67", gatewaysManager.selectVpnProfile(1).mGatewayIp);
+ assertEquals("212.129.62.247", gatewaysManager.selectVpnProfile(2).mGatewayIp);
}
@Test
@@ -358,9 +388,9 @@ public class GatewaysManagerTest {
commit();
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
- assertEquals("mouette.riseup.net", gatewaysManager.select(0).gateway.getHost());
- assertEquals("hoatzin.riseup.net", gatewaysManager.select(1).gateway.getHost());
- assertEquals("zarapito.riseup.net", gatewaysManager.select(2).gateway.getHost());
+ assertEquals("163.172.126.44", gatewaysManager.selectVpnProfile(0).mGatewayIp);
+ assertEquals("212.83.143.67", gatewaysManager.selectVpnProfile(1).mGatewayIp);
+ assertEquals("212.129.62.247", gatewaysManager.selectVpnProfile(2).mGatewayIp);
}
@Test
@@ -375,10 +405,10 @@ public class GatewaysManagerTest {
commit();
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
- assertEquals("Paris", gatewaysManager.select(0).gateway.getName());
- assertEquals("Paris", gatewaysManager.select(1).gateway.getName());
- assertEquals("Paris", gatewaysManager.select(2).gateway.getName());
- assertEquals(null, gatewaysManager.select(3));
+ assertEquals("Paris", gatewaysManager.selectVpnProfile(0).getName());
+ assertEquals("Paris", gatewaysManager.selectVpnProfile(1).getName());
+ assertEquals("Paris", gatewaysManager.selectVpnProfile(2).getName());
+ assertEquals(null, gatewaysManager.selectVpnProfile(3));
}
@Test
@@ -388,9 +418,9 @@ public class GatewaysManagerTest {
providerObservable.updateProvider(provider);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
- assertEquals("mouette.riseup.net", gatewaysManager.select(0, "Paris").gateway.getHost());
- assertEquals("hoatzin.riseup.net", gatewaysManager.select(1, "Paris").gateway.getHost());
- assertEquals("zarapito.riseup.net", gatewaysManager.select(2, "Paris").gateway.getHost());
+ assertEquals("163.172.126.44", gatewaysManager.selectVpnProfile(0, "Paris").mGatewayIp);
+ assertEquals("212.83.143.67", gatewaysManager.selectVpnProfile(1, "Paris").mGatewayIp);
+ assertEquals("212.129.62.247", gatewaysManager.selectVpnProfile(2, "Paris").mGatewayIp);
}
@Test
@@ -400,9 +430,9 @@ public class GatewaysManagerTest {
providerObservable.updateProvider(provider);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
- assertEquals("mouette.riseup.net", gatewaysManager.select(0, "Paris").gateway.getHost());
- assertEquals("hoatzin.riseup.net", gatewaysManager.select(1, "Paris").gateway.getHost());
- assertEquals("zarapito.riseup.net", gatewaysManager.select(2, "Paris").gateway.getHost());
+ assertEquals("163.172.126.44", gatewaysManager.selectVpnProfile(0, "Paris").mGatewayIp);
+ assertEquals("212.83.143.67", gatewaysManager.selectVpnProfile(1, "Paris").mGatewayIp);
+ assertEquals("212.129.62.247", gatewaysManager.selectVpnProfile(2, "Paris").mGatewayIp);
}
@Test
@@ -413,10 +443,10 @@ public class GatewaysManagerTest {
providerObservable.updateProvider(provider);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
- assertEquals("Paris", gatewaysManager.select(0, "Paris").gateway.getName());
- assertEquals("Paris", gatewaysManager.select(1, "Paris").gateway.getName());
- assertEquals("Paris", gatewaysManager.select(2, "Paris").gateway.getName());
- assertEquals(null, gatewaysManager.select(3, "Paris"));
+ assertEquals("Paris", gatewaysManager.selectVpnProfile(0, "Paris").getName());
+ assertEquals("Paris", gatewaysManager.selectVpnProfile(1, "Paris").getName());
+ assertEquals("Paris", gatewaysManager.selectVpnProfile(2, "Paris").getName());
+ assertEquals(null, gatewaysManager.selectVpnProfile(3, "Paris"));
}
@Test
@@ -426,7 +456,7 @@ public class GatewaysManagerTest {
provider.setGeoIpJson(new JSONObject());
providerObservable.updateProvider(provider);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
- assertNull(gatewaysManager.select(0, "Stockholm"));
+ assertNull(gatewaysManager.selectVpnProfile(0, "Stockholm"));
}
@Test
diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java
index 7581a395..fa5d888b 100644
--- a/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java
@@ -8,26 +8,35 @@ import static org.mockito.Mockito.when;
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.IP_ADDRESS;
+import static se.leap.bitmaskclient.base.models.Constants.IP_ADDRESS6;
import static se.leap.bitmaskclient.base.models.Constants.OPENVPN_CONFIGURATION;
+import static se.leap.bitmaskclient.base.models.Transport.createTransportsFrom;
+import static se.leap.bitmaskclient.eip.VpnConfigGenerator.Configuration.createProfileConfig;
import android.content.Context;
import android.content.SharedPreferences;
+import android.os.Build;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
import java.io.File;
-import java.util.HashMap;
+import java.util.Vector;
import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.ConfigParser;
import de.blinkt.openvpn.core.connection.Connection;
import de.blinkt.openvpn.core.connection.Obfs4Connection;
import se.leap.bitmaskclient.base.models.ProviderObservable;
+import se.leap.bitmaskclient.base.models.Transport;
import se.leap.bitmaskclient.base.utils.BuildConfigHelper;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
-import se.leap.bitmaskclient.base.utils.RSAHelper;
+import se.leap.bitmaskclient.base.utils.PrivateKeyHelper;
import se.leap.bitmaskclient.testutils.MockHelper;
import se.leap.bitmaskclient.testutils.MockSharedPreferences;
import se.leap.bitmaskclient.testutils.TestSetupHelper;
@@ -35,6 +44,8 @@ import se.leap.bitmaskclient.testutils.TestSetupHelper;
/**
* Created by cyberta on 03.10.17.
*/
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = {Build.VERSION_CODES.P})
public class VpnConfigGeneratorTest {
Context context;
@@ -54,7 +65,7 @@ public class VpnConfigGeneratorTest {
"management-hold\n" +
"\n" +
"setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
- "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+ "setenv IV_PLAT_VER \"28 9 ROBO Android unknown robolectric\"\n" +
"machine-readable-output\n" +
"allow-recursive-routing\n" +
"ifconfig-nowarn\n" +
@@ -150,7 +161,7 @@ public class VpnConfigGeneratorTest {
"management-hold\n" +
"\n" +
"setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
- "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+ "setenv IV_PLAT_VER \"28 9 ROBO Android unknown robolectric\"\n" +
"machine-readable-output\n" +
"allow-recursive-routing\n" +
"ifconfig-nowarn\n" +
@@ -246,7 +257,7 @@ public class VpnConfigGeneratorTest {
"management-hold\n" +
"\n" +
"setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
- "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+ "setenv IV_PLAT_VER \"28 9 ROBO Android unknown robolectric\"\n" +
"machine-readable-output\n" +
"allow-recursive-routing\n" +
"ifconfig-nowarn\n" +
@@ -342,7 +353,7 @@ public class VpnConfigGeneratorTest {
"management-hold\n" +
"\n" +
"setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
- "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+ "setenv IV_PLAT_VER \"28 9 ROBO Android unknown robolectric\"\n" +
"machine-readable-output\n" +
"allow-recursive-routing\n" +
"ifconfig-nowarn\n" +
@@ -438,7 +449,7 @@ public class VpnConfigGeneratorTest {
"management-hold\n" +
"\n" +
"setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
- "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+ "setenv IV_PLAT_VER \"28 9 ROBO Android unknown robolectric\"\n" +
"machine-readable-output\n" +
"allow-recursive-routing\n" +
"ifconfig-nowarn\n" +
@@ -534,7 +545,7 @@ public class VpnConfigGeneratorTest {
"management-hold\n" +
"\n" +
"setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
- "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+ "setenv IV_PLAT_VER \"28 9 ROBO Android unknown robolectric\"\n" +
"machine-readable-output\n" +
"allow-recursive-routing\n" +
"ifconfig-nowarn\n" +
@@ -630,7 +641,7 @@ public class VpnConfigGeneratorTest {
"management-hold\n" +
"\n" +
"setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
- "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+ "setenv IV_PLAT_VER \"28 9 ROBO Android unknown robolectric\"\n" +
"machine-readable-output\n" +
"allow-recursive-routing\n" +
"ifconfig-nowarn\n" +
@@ -726,7 +737,7 @@ public class VpnConfigGeneratorTest {
"management-hold\n" +
"\n" +
"setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
- "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+ "setenv IV_PLAT_VER \"28 9 ROBO Android unknown robolectric\"\n" +
"machine-readable-output\n" +
"allow-recursive-routing\n" +
"ifconfig-nowarn\n" +
@@ -828,7 +839,7 @@ public class VpnConfigGeneratorTest {
"management-hold\n" +
"\n" +
"setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
- "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+ "setenv IV_PLAT_VER \"28 9 ROBO Android unknown robolectric\"\n" +
"machine-readable-output\n" +
"allow-recursive-routing\n" +
"ifconfig-nowarn\n" +
@@ -930,7 +941,7 @@ public class VpnConfigGeneratorTest {
"management-hold\n" +
"\n" +
"setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
- "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+ "setenv IV_PLAT_VER \"28 9 ROBO Android unknown robolectric\"\n" +
"machine-readable-output\n" +
"allow-recursive-routing\n" +
"ifconfig-nowarn\n" +
@@ -1041,7 +1052,7 @@ public class VpnConfigGeneratorTest {
"management-hold\n" +
"\n" +
"setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
- "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+ "setenv IV_PLAT_VER \"28 9 ROBO Android unknown robolectric\"\n" +
"machine-readable-output\n" +
"allow-recursive-routing\n" +
"ifconfig-nowarn\n" +
@@ -1144,7 +1155,7 @@ public class VpnConfigGeneratorTest {
"management-hold\n" +
"\n" +
"setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
- "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+ "setenv IV_PLAT_VER \"28 9 ROBO Android unknown robolectric\"\n" +
"machine-readable-output\n" +
"allow-recursive-routing\n" +
"ifconfig-nowarn\n" +
@@ -1245,7 +1256,7 @@ public class VpnConfigGeneratorTest {
"management-hold\n" +
"\n" +
"setenv IV_GUI_VER \"se.leap.bitmaskclient 0.9.10\" \n" +
- "setenv IV_PLAT_VER \"0 null JUNIT null null null\"\n" +
+ "setenv IV_PLAT_VER \"28 9 ROBO Android unknown robolectric\"\n" +
"machine-readable-output\n" +
"allow-recursive-routing\n" +
"ifconfig-nowarn\n" +
@@ -1347,54 +1358,69 @@ public class VpnConfigGeneratorTest {
context = MockHelper.mockContext();
ProviderObservable providerObservable = MockHelper.mockProviderObservable(TestSetupHelper.getConfiguredProvider());
- RSAHelper rsaHelper = MockHelper.mockRSAHelper();
+ PrivateKeyHelper privateKeyHelper = MockHelper.mockPrivateKeyHelper();
sharedPreferences = new MockSharedPreferences();
preferenceHelper = new PreferenceHelper(new MockSharedPreferences());
when(context.getCacheDir()).thenReturn(new File("/data/data/se.leap.bitmask"));
}
+ private static boolean containsKey(Vector<VpnProfile> profiles, Connection.TransportType transportType) {
+ for (VpnProfile profile : profiles) {
+ if (profile.getTransportType() == transportType) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static VpnProfile getVpnProfile(Vector<VpnProfile> profiles, Connection.TransportType transportType) {
+ for (VpnProfile profile : profiles) {
+ if (profile.getTransportType() == transportType) {
+ return profile;
+ }
+ }
+ return null;
+ }
+
@Test
public void testGenerateVpnProfile_v1_tcp_udp() throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_tcp_udp.json")));
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 1;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertFalse(vpnProfiles.containsKey(OBFS4));
- assertEquals(expectedVPNConfig_v1_tcp_udp.trim(), vpnProfiles.get(OPENVPN).getConfigFile(context, false).trim());
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 1), 1, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertFalse(containsKey(vpnProfiles, OBFS4));
+ assertEquals(expectedVPNConfig_v1_tcp_udp.trim(), getVpnProfile(vpnProfiles, OPENVPN).getConfigFile(context, false).trim());
}
@Test
public void testGenerateVpnProfile_v1_udp_tcp() throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_udp_tcp.json")));
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 1;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertFalse(vpnProfiles.containsKey(OBFS4));
- assertEquals(expectedVPNConfig_v1_udp_tcp.trim(), vpnProfiles.get(OPENVPN).getConfigFile(context, false).trim());
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 1), 1, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertFalse(containsKey(vpnProfiles, OBFS4));
+ assertEquals(expectedVPNConfig_v1_udp_tcp.trim(), getVpnProfile(vpnProfiles, OPENVPN).getConfigFile(context, false).trim());
}
@Test
public void testGenerateVpnProfile_v2_tcp_udp() throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_tcp_udp.json")));
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 2;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertFalse(vpnProfiles.containsKey(OBFS4));
- assertEquals(expectedVPNConfig_v1_tcp_udp.trim(), vpnProfiles.get(OPENVPN).getConfigFile(context, false).trim());
+
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 2), 2, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertFalse(containsKey(vpnProfiles, OBFS4));
+ assertEquals(expectedVPNConfig_v1_tcp_udp.trim(), getVpnProfile(vpnProfiles, OPENVPN).getConfigFile(context, false).trim());
}
@Test
public void testGenerateVpnProfile_v2_udp_tcp() throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_udp_tcp.json")));
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 2;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertFalse(vpnProfiles.containsKey(OBFS4));
- assertEquals(expectedVPNConfig_v1_udp_tcp.trim(), vpnProfiles.get(OPENVPN).getConfigFile(context, false).trim());
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 2), 2, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertFalse(containsKey(vpnProfiles, OBFS4));
+ assertEquals(expectedVPNConfig_v1_udp_tcp.trim(), getVpnProfile(vpnProfiles, OPENVPN).getConfigFile(context, false).trim());
}
@@ -1402,54 +1428,51 @@ public class VpnConfigGeneratorTest {
public void testGenerateVpnProfile_v3_obfs4() throws Exception {
BuildConfigHelper buildConfigHelper = MockHelper.mockBuildConfigHelper(false);
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo.bitmask.eip-service-obfsvpn1.0.0.json"))).getJSONArray("gateways").getJSONObject(0);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertTrue(vpnProfiles.containsKey(OBFS4));
- assertTrue(vpnProfiles.containsKey(OPENVPN));
- System.out.println(vpnProfiles.get(OBFS4).getConfigFile(context, false));
- assertEquals(expectedVPNConfig_v3_obfs4.trim(), vpnProfiles.get(OBFS4).getConfigFile(context, false).trim());
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(containsKey(vpnProfiles, OBFS4));
+ assertTrue(containsKey(vpnProfiles, OPENVPN));
+ System.out.println(getVpnProfile(vpnProfiles, OBFS4).getConfigFile(context, false));
+ assertEquals(expectedVPNConfig_v3_obfs4.trim(), getVpnProfile(vpnProfiles, OBFS4).getConfigFile(context, false).trim());
}
@Test
public void testGenerateVpnProfile_v3_obfs4_obfsvpn() throws Exception {
BuildConfigHelper buildConfigHelper = MockHelper.mockBuildConfigHelper(true);
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo.bitmask.eip-service-obfsvpn1.0.0.json"))).getJSONArray("gateways").getJSONObject(0);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertTrue(vpnProfiles.containsKey(OBFS4));
- assertTrue(vpnProfiles.containsKey(OPENVPN));
- System.out.println(vpnProfiles.get(OBFS4).getConfigFile(context, false));
- assertEquals(expectedVPNConfig_v3_obfsvpn_obfs4.trim(), vpnProfiles.get(OBFS4).getConfigFile(context, false).trim());
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(containsKey(vpnProfiles, OBFS4));
+ assertTrue(containsKey(vpnProfiles, OPENVPN));
+ System.out.println(getVpnProfile(vpnProfiles, OBFS4).getConfigFile(context, false));
+ assertEquals(expectedVPNConfig_v3_obfsvpn_obfs4.trim(), getVpnProfile(vpnProfiles, OBFS4).getConfigFile(context, false).trim());
}
@Test
public void testGenerateVpnProfile_v3_ovpn_tcp_udp() throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_pt_tcp_udp.eip-service.json"))).getJSONArray("gateways").getJSONObject(0);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertTrue(vpnProfiles.containsKey(OBFS4));
- assertTrue(vpnProfiles.containsKey(OPENVPN));
- System.out.println(vpnProfiles.get(OPENVPN).getConfigFile(context, false));
- assertEquals(expectedVPNConfig_v3_ovpn_tcp_udp.trim(), vpnProfiles.get(OPENVPN).getConfigFile(context, false).trim());
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
+ configuration.preferUDP = false;
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(containsKey(vpnProfiles, OBFS4));
+ assertTrue(containsKey(vpnProfiles, OPENVPN));
+ System.out.println(getVpnProfile(vpnProfiles, OPENVPN).getConfigFile(context, false));
+ assertEquals(expectedVPNConfig_v3_ovpn_tcp_udp.trim(), getVpnProfile(vpnProfiles, OPENVPN).getConfigFile(context, false).trim());
}
@Test
public void testGenerateVpnProfile_v3_ovpn_udp_tcp() throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_pt_udp_tcp.eip-service.json"))).getJSONArray("gateways").getJSONObject(0);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertTrue(vpnProfiles.containsKey(OBFS4));
- assertTrue(vpnProfiles.containsKey(OPENVPN));
- System.out.println(vpnProfiles.get(OPENVPN).getConfigFile(context, false));
- assertEquals(expectedVPNConfig_v3_ovpn_udp_tcp.trim(), vpnProfiles.get(OPENVPN).getConfigFile(context, false).trim());
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(containsKey(vpnProfiles, OBFS4));
+ assertTrue(containsKey(vpnProfiles, OPENVPN));
+ System.out.println(getVpnProfile(vpnProfiles, OPENVPN).getConfigFile(context, false));
+ assertEquals(expectedVPNConfig_v3_ovpn_udp_tcp.trim(), getVpnProfile(vpnProfiles, OPENVPN).getConfigFile(context, false).trim());
}
@Test
@@ -1457,64 +1480,60 @@ public class VpnConfigGeneratorTest {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_pt_udp_tcp.eip-service.json"))).getJSONArray("gateways").getJSONObject(0);
//delete "data-ciphers" from config to test if the resulting openvpn config file will contain the default value taken from "cipher" flag
generalConfig.put("data-ciphers", null);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertTrue(vpnProfiles.containsKey(OBFS4));
- assertTrue(vpnProfiles.containsKey(OPENVPN));
- System.out.println(vpnProfiles.get(OPENVPN).getConfigFile(context, false));
- assertEquals(expectedVPNConfig_v3_ovpn_udp_tcp_defaultDataCiphers.trim(), vpnProfiles.get(OPENVPN).getConfigFile(context, false).trim());
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(containsKey(vpnProfiles, OBFS4));
+ assertTrue(containsKey(vpnProfiles, OPENVPN));
+ System.out.println(getVpnProfile(vpnProfiles, OPENVPN).getConfigFile(context, false));
+ assertEquals(expectedVPNConfig_v3_ovpn_udp_tcp_defaultDataCiphers.trim(), getVpnProfile(vpnProfiles, OPENVPN).getConfigFile(context, false).trim());
}
@Test
public void testGenerateVpnProfile_v4_ovpn_tcp_udp() throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("v4/ptdemo_pt_tcp_udp.eip-service.json"))).getJSONArray("gateways").getJSONObject(0);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("v4/ptdemo_pt_tcp_udp.eip-service.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 4;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertTrue(vpnProfiles.containsKey(OBFS4));
- assertTrue(vpnProfiles.containsKey(OPENVPN));
- System.out.println(vpnProfiles.get(OPENVPN).getConfigFile(context, false));
- assertEquals(expectedVPNConfig_v4_ovpn_tcp_udp.trim(), vpnProfiles.get(OPENVPN).getConfigFile(context, false).trim());
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 4), 4, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
+ configuration.preferUDP = false;
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(containsKey(vpnProfiles, OBFS4));
+ assertTrue(containsKey(vpnProfiles, OPENVPN));
+ System.out.println(getVpnProfile(vpnProfiles, OPENVPN).getConfigFile(context, false));
+ assertEquals(expectedVPNConfig_v4_ovpn_tcp_udp.trim(), getVpnProfile(vpnProfiles, OPENVPN).getConfigFile(context, false).trim());
}
@Test
public void testGenerateVpnProfile_v4_ovpn_udp_tcp() throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("v4/ptdemo_pt_udp_tcp.eip-service.json"))).getJSONArray("gateways").getJSONObject(0);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("v4/ptdemo_pt_udp_tcp.eip-service.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 4;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertTrue(vpnProfiles.containsKey(OBFS4));
- assertTrue(vpnProfiles.containsKey(OPENVPN));
- System.out.println(vpnProfiles.get(OPENVPN).getConfigFile(context, false));
- assertEquals(expectedVPNConfig_v4_ovpn_udp_tcp.trim(), vpnProfiles.get(OPENVPN).getConfigFile(context, false).trim());
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 4), 4, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(containsKey(vpnProfiles, OBFS4));
+ assertTrue(containsKey(vpnProfiles, OPENVPN));
+ System.out.println(getVpnProfile(vpnProfiles, OPENVPN).getConfigFile(context, false));
+ assertEquals(expectedVPNConfig_v4_ovpn_udp_tcp.trim(), getVpnProfile(vpnProfiles, OPENVPN).getConfigFile(context, false).trim());
}
@Test
public void testGenerateVpnProfile_v3_ipv6only_allowOpenvpnIPv6Only() throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_misconfigured_ipv6.json"))).getJSONArray("gateways").getJSONObject(0);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_misconfigured_ipv6.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertTrue(vpnProfiles.containsKey(OPENVPN));
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(containsKey(vpnProfiles, OPENVPN));
}
@Test
public void testGenerateVpnProfile_v3_obfs4IPv6_skip() throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_misconfigured_ipv6.json"))).getJSONArray("gateways").getJSONObject(0);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_misconfigured_ipv6.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertFalse(vpnProfiles.containsKey(OBFS4));
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertFalse(containsKey(vpnProfiles, OBFS4));
}
/**
@@ -1524,14 +1543,13 @@ public class VpnConfigGeneratorTest {
public void testGenerateVpnProfile_v3_obfs4IPv4AndIPv6_skipIPv6() throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_misconfigured_ipv4ipv6.json"))).getJSONArray("gateways").getJSONObject(0);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_misconfigured_ipv4ipv6.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertTrue(vpnProfiles.containsKey(OBFS4));
- assertTrue(vpnProfiles.containsKey(OPENVPN));
- assertEquals(1, vpnProfiles.get(OBFS4).mConnections.length);
- assertEquals("37.218.247.60/32", vpnProfiles.get(OBFS4).mExcludedRoutes.trim());
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(containsKey(vpnProfiles, OBFS4));
+ assertTrue(containsKey(vpnProfiles, OPENVPN));
+ assertEquals(1, getVpnProfile(vpnProfiles, OBFS4).mConnections.length);
+ assertEquals("37.218.247.60/32", getVpnProfile(vpnProfiles, OBFS4).mExcludedRoutes.trim());
}
/**
@@ -1541,12 +1559,11 @@ public class VpnConfigGeneratorTest {
public void testGenerateVpnProfile_v3_obfs4udp_skip() throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_misconfigured_udp.json"))).getJSONArray("gateways").getJSONObject(0);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_misconfigured_udp.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertFalse(vpnProfiles.containsKey(OBFS4));
- assertTrue(vpnProfiles.containsKey(OPENVPN));
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertFalse(containsKey(vpnProfiles, OBFS4));
+ assertTrue(containsKey(vpnProfiles, OPENVPN));
}
/**
@@ -1556,79 +1573,74 @@ public class VpnConfigGeneratorTest {
public void testGenerateVpnProfile_v3_obfs4TCP_openvpnTCP_skip() throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_misconfigured_tcp2.json"))).getJSONArray("gateways").getJSONObject(0);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_misconfigured_tcp2.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertFalse(vpnProfiles.containsKey(OBFS4));
- assertTrue(vpnProfiles.containsKey(OPENVPN));
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertFalse(containsKey(vpnProfiles, OBFS4));
+ assertTrue(containsKey(vpnProfiles, OPENVPN));
}
@Test
public void testGenerateVpnProfile_v3_obfs4UDPAndTCP_skipUDP() throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_misconfigured_udptcp.json"))).getJSONArray("gateways").getJSONObject(0);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_misconfigured_udptcp.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertTrue(vpnProfiles.containsKey(OBFS4));
- assertTrue(vpnProfiles.containsKey(OPENVPN));
- assertEquals(1, vpnProfiles.get(OBFS4).mConnections.length);
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(containsKey(vpnProfiles, OBFS4));
+ assertTrue(containsKey(vpnProfiles, OPENVPN));
+ assertEquals(1, getVpnProfile(vpnProfiles, OBFS4).mConnections.length);
}
@Test
public void testGenerateVpnProfile_preferUDP_firstRemotesUDP() throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("v4/multiport_tcpudp_eip-service.json"))).getJSONArray("gateways").getJSONObject(0);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("v4/multiport_tcpudp_eip-service.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
configuration.preferUDP = true;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertTrue(vpnProfiles.containsKey(OBFS4));
- assertTrue(vpnProfiles.containsKey(OPENVPN));
- System.out.println(vpnProfiles.get(OPENVPN).getConfigFile(context, false));
- assertEquals(expectedVPNConfig_v4_ovpn_multiport_tcpudp.trim(), vpnProfiles.get(OPENVPN).getConfigFile(context, false).trim());
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(containsKey(vpnProfiles, OBFS4));
+ assertTrue(containsKey(vpnProfiles, OPENVPN));
+ System.out.println(getVpnProfile(vpnProfiles, OPENVPN).getConfigFile(context, false));
+ assertEquals(expectedVPNConfig_v4_ovpn_multiport_tcpudp.trim(), getVpnProfile(vpnProfiles, OPENVPN).getConfigFile(context, false).trim());
}
@Test
public void testGenerateVpnProfile_testNewCiphers() throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("v4/ptdemo_pt_tcp_udp_new_ciphers.eip-service.json"))).getJSONArray("gateways").getJSONObject(0);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("v4/ptdemo_pt_tcp_udp_new_ciphers.eip-service.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 4;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- System.out.println(vpnProfiles.get(OPENVPN).getConfigFile(context, false));
- assertEquals(expectedVPNConfig_v4_ovpn_tcp_udp_new_ciphers.trim(), vpnProfiles.get(OPENVPN).getConfigFile(context, false).trim());
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 4), 4, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
+ configuration.preferUDP = false;
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ System.out.println(getVpnProfile(vpnProfiles, OPENVPN).getConfigFile(context, false));
+ assertEquals(expectedVPNConfig_v4_ovpn_tcp_udp_new_ciphers.trim(), getVpnProfile(vpnProfiles, OPENVPN).getConfigFile(context, false).trim());
}
@Test
public void testGenerateVpnProfileExperimentalTransportsEnabled () throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_kcp_gateways.json"))).getJSONArray("gateways").getJSONObject(2);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_kcp_gateways.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
configuration.experimentalTransports = true;
configuration.preferUDP = true;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertTrue(vpnProfiles.containsKey(OBFS4) && ((Obfs4Connection)vpnProfiles.get(OBFS4).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("kcp"));
- assertTrue(vpnProfiles.containsKey(OPENVPN));
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(containsKey(vpnProfiles, OBFS4) && ((Obfs4Connection)getVpnProfile(vpnProfiles, OBFS4).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("kcp"));
+ assertTrue(containsKey(vpnProfiles, OPENVPN));
}
@Test
public void testGenerateVpnProfile_experimentalTransportsEnabled_KCPMisconfiguredWithUDP_SkippingObfsKCP () throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_misconfigured_kcp_gateways.json"))).getJSONArray("gateways").getJSONObject(2);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_misconfigured_kcp_gateways.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
configuration.experimentalTransports = true;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertFalse(vpnProfiles.containsKey(OBFS4));
- assertTrue(vpnProfiles.containsKey(OPENVPN));
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertFalse(containsKey(vpnProfiles, OBFS4));
+ assertTrue(containsKey(vpnProfiles, OPENVPN));
}
@Test
@@ -1641,13 +1653,14 @@ public class VpnConfigGeneratorTest {
configuration.obfuscationProxyPort = "443";
configuration.obfuscationProxyIP = "5.6.7.8";
configuration.obfuscationProxyCert = "asdfasdf";
- configuration.obfuscationProxyKCP = true;
+ configuration.obfuscationProxyTransportProtocol = "kcp";
configuration.remoteGatewayIP = "1.2.3.4";
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertTrue("has openvpn profile", vpnProfiles.containsKey(OPENVPN));
- assertTrue("has obfs4 profile", vpnProfiles.containsKey(OBFS4));
- assertTrue("bridge is running KCP", vpnProfiles.get(OBFS4).mGatewayIp.equals("1.2.3.4"));
+ configuration.transports = createTransportsFrom(gateway, 3);
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue("has openvpn profile", containsKey(vpnProfiles, OPENVPN));
+ assertTrue("has obfs4 profile", containsKey(vpnProfiles, OBFS4));
+ assertTrue("bridge is running KCP", getVpnProfile(vpnProfiles, OBFS4).mGatewayIp.equals("1.2.3.4"));
}
@@ -1658,17 +1671,18 @@ public class VpnConfigGeneratorTest {
VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
configuration.apiVersion = 3;
configuration.useObfuscationPinning = true;
- configuration.obfuscationProxyKCP = false;
+ configuration.obfuscationProxyTransportProtocol = "tcp";
configuration.obfuscationProxyPort = "443";
configuration.obfuscationProxyIP = "5.6.7.8";
configuration.obfuscationProxyCert = "asdfasdf";
configuration.remoteGatewayIP = "1.2.3.4";
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertFalse("has openvpn profile", vpnProfiles.containsKey(OPENVPN));
- assertTrue("has obfs4 profile", vpnProfiles.containsKey(OBFS4));
- assertTrue("bridge is pinned one", vpnProfiles.get(OBFS4).getTransportType() == OBFS4 && vpnProfiles.get(OBFS4).mConnections[0].isUseUdp());
- assertTrue("bridge is running TCP", ((Obfs4Connection) vpnProfiles.get(OBFS4).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("tcp"));
+ configuration.transports = createTransportsFrom(gateway, 3);
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertFalse("has openvpn profile", containsKey(vpnProfiles, OPENVPN));
+ assertTrue("has obfs4 profile", containsKey(vpnProfiles, OBFS4));
+ assertTrue("bridge is pinned one", getVpnProfile(vpnProfiles, OBFS4).getTransportType() == OBFS4 && getVpnProfile(vpnProfiles, OBFS4).mConnections[0].isUseUdp());
+ assertTrue("bridge is running TCP", ((Obfs4Connection) getVpnProfile(vpnProfiles, OBFS4).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("tcp"));
}
@Test
@@ -1678,43 +1692,64 @@ public class VpnConfigGeneratorTest {
VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
configuration.apiVersion = 3;
configuration.useObfuscationPinning = true;
- configuration.obfuscationProxyKCP = true;
+ configuration.obfuscationProxyTransportProtocol = "kcp";
configuration.obfuscationProxyPort = "443";
configuration.obfuscationProxyIP = "5.6.7.8";
configuration.obfuscationProxyCert = "asdfasdf";
configuration.remoteGatewayIP = "1.2.3.4";
+ configuration.transports = createTransportsFrom(gateway, 3);
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertFalse("has openvpn profile", vpnProfiles.containsKey(OPENVPN));
- assertTrue("has no obfs4 profile", vpnProfiles.containsKey(OBFS4));
- assertTrue("bridge is pinned one", vpnProfiles.get(OBFS4).getTransportType() == OBFS4 && vpnProfiles.get(OBFS4).mGatewayIp.equals("1.2.3.4"));
- assertTrue("bridge is running KCP", ((Obfs4Connection) vpnProfiles.get(OBFS4).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("kcp"));
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertFalse("has openvpn profile", containsKey(vpnProfiles, OPENVPN));
+ assertTrue("has no obfs4 profile", containsKey(vpnProfiles, OBFS4));
+ assertTrue("bridge is pinned one", getVpnProfile(vpnProfiles, OBFS4).getTransportType() == OBFS4 && getVpnProfile(vpnProfiles, OBFS4).mGatewayIp.equals("1.2.3.4"));
+ assertTrue("bridge is running KCP", ((Obfs4Connection) getVpnProfile(vpnProfiles, OBFS4).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("kcp"));
+ }
+
+ @Test
+ public void testGenerateVpnProfile_ObfuscationPinningEnabled_quic_obfs4QuicProfile () throws Exception {
+ gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_kcp_gateways.json"))).getJSONArray("gateways").getJSONObject(0);
+ generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_kcp_gateways.json"))).getJSONObject(OPENVPN_CONFIGURATION);
+ VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
+ configuration.apiVersion = 3;
+ configuration.useObfuscationPinning = true;
+ configuration.obfuscationProxyTransportProtocol = "quic";
+ configuration.obfuscationProxyPort = "443";
+ configuration.obfuscationProxyIP = "5.6.7.8";
+ configuration.obfuscationProxyCert = "asdfasdf";
+ configuration.remoteGatewayIP = "1.2.3.4";
+ configuration.transports = createTransportsFrom(gateway, 3);
+
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertFalse("has openvpn profile", containsKey(vpnProfiles, OPENVPN));
+ assertTrue("has no obfs4 profile", containsKey(vpnProfiles, OBFS4));
+ assertTrue("bridge is pinned one", getVpnProfile(vpnProfiles, OBFS4).getTransportType() == OBFS4 && getVpnProfile(vpnProfiles, OBFS4).mGatewayIp.equals("1.2.3.4"));
+ assertTrue("bridge is running QUIC", ((Obfs4Connection) getVpnProfile(vpnProfiles, OBFS4).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("quic"));
}
@Test
public void testGenerateVpnProfile_obfs4hop_tcp () throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_obfs4hop_tcp_gateways.json"))).getJSONArray("gateways").getJSONObject(2);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_obfs4hop_tcp_gateways.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
configuration.experimentalTransports = true;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertTrue(vpnProfiles.containsKey(OBFS4_HOP) && ((Obfs4Connection)vpnProfiles.get(OBFS4_HOP).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("tcp"));
- assertTrue(vpnProfiles.containsKey(OPENVPN));
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(containsKey(vpnProfiles, OBFS4_HOP) && ((Obfs4Connection)getVpnProfile(vpnProfiles, OBFS4_HOP).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("tcp"));
+ assertTrue(containsKey(vpnProfiles, OPENVPN));
}
@Test
public void testGenerateVpnProfile_obfs4hop_kcp () throws Exception {
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_obfs4hop_kcp_gateways.json"))).getJSONArray("gateways").getJSONObject(2);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("ptdemo_obfs4hop_kcp_gateways.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
configuration.experimentalTransports = true;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertTrue(vpnProfiles.containsKey(OBFS4_HOP) && ((Obfs4Connection)vpnProfiles.get(OBFS4_HOP).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("kcp"));
- assertTrue(vpnProfiles.containsKey(OPENVPN));
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(containsKey(vpnProfiles, OBFS4_HOP) && ((Obfs4Connection)getVpnProfile(vpnProfiles, OBFS4_HOP).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("kcp"));
+ assertTrue(containsKey(vpnProfiles, OPENVPN));
}
@Test
@@ -1722,13 +1757,12 @@ public class VpnConfigGeneratorTest {
BuildConfigHelper buildConfigHelper = MockHelper.mockBuildConfigHelper(true);
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt_portHopping.eip-service.json"))).getJSONArray("gateways").getJSONObject(2);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt_portHopping.eip-service.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
configuration.experimentalTransports = true;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- System.out.println(vpnProfiles.get(OBFS4_HOP).getConfigFile(context, false));
- assertEquals(expectedVPNConfig_hopping_pt_portHopping.trim(), vpnProfiles.get(OBFS4_HOP).getConfigFile(context, false).trim());
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ System.out.println(getVpnProfile(vpnProfiles, OBFS4_HOP).getConfigFile(context, false));
+ assertEquals(expectedVPNConfig_hopping_pt_portHopping.trim(), getVpnProfile(vpnProfiles, OBFS4_HOP).getConfigFile(context, false).trim());
}
@Test
@@ -1736,27 +1770,25 @@ public class VpnConfigGeneratorTest {
BuildConfigHelper buildConfigHelper = MockHelper.mockBuildConfigHelper(true);
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt_portHopping.eip-service.json"))).getJSONArray("gateways").getJSONObject(2);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt_portHopping.eip-service.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
configuration.experimentalTransports = true;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- System.out.println(vpnProfiles.get(OBFS4_HOP).getConfigFile(context, false));
- assertEquals(expectedVPNConfig_hopping_pt_portHopping.trim(), vpnProfiles.get(OBFS4_HOP).getConfigFile(context, false).trim());
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ System.out.println(getVpnProfile(vpnProfiles, OBFS4_HOP).getConfigFile(context, false));
+ assertEquals(expectedVPNConfig_hopping_pt_portHopping.trim(), getVpnProfile(vpnProfiles, OBFS4_HOP).getConfigFile(context, false).trim());
}
@Test
public void testGenerateVpnProfile_obfs4_decoupled() throws Exception {
BuildConfigHelper buildConfigHelper = MockHelper.mockBuildConfigHelper(true);
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt.eip-service.json"))).getJSONArray("gateways").getJSONObject(1);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt.eip-service.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
configuration.experimentalTransports = true;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertTrue(vpnProfiles.containsKey(OBFS4));
- assertTrue(((Obfs4Connection)vpnProfiles.get(OBFS4).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("tcp"));
- assertFalse(vpnProfiles.containsKey(OPENVPN));
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(containsKey(vpnProfiles, OBFS4));
+ assertTrue(((Obfs4Connection)getVpnProfile(vpnProfiles, OBFS4).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("tcp"));
+ assertFalse(containsKey(vpnProfiles, OPENVPN));
}
@Test
@@ -1764,15 +1796,14 @@ public class VpnConfigGeneratorTest {
BuildConfigHelper buildConfigHelper = MockHelper.mockBuildConfigHelper(true);
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt.eip-service.json"))).getJSONArray("gateways").getJSONObject(2);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt.eip-service.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
configuration.experimentalTransports = true;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertTrue(vpnProfiles.containsKey(OBFS4_HOP));
- assertTrue(((Obfs4Connection)vpnProfiles.get(OBFS4_HOP).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("tcp"));
- assertFalse(vpnProfiles.containsKey(OPENVPN));
- assertFalse(vpnProfiles.containsKey(OBFS4));
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(containsKey(vpnProfiles, OBFS4_HOP));
+ assertTrue(((Obfs4Connection)getVpnProfile(vpnProfiles, OBFS4_HOP).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("tcp"));
+ assertFalse(containsKey(vpnProfiles, OPENVPN));
+ assertFalse(containsKey(vpnProfiles, OBFS4));
}
@Test
@@ -1780,10 +1811,9 @@ public class VpnConfigGeneratorTest {
BuildConfigHelper buildConfigHelper = MockHelper.mockBuildConfigHelper(true);
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt.eip-service.json"))).getJSONArray("gateways").getJSONObject(2);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt.eip-service.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
configuration.experimentalTransports = false;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
Exception exception = null;
try {
vpnConfigGenerator.generateVpnProfiles();
@@ -1798,14 +1828,13 @@ public class VpnConfigGeneratorTest {
BuildConfigHelper buildConfigHelper = MockHelper.mockBuildConfigHelper(true);
gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt_portHopping.eip-service.json"))).getJSONArray("gateways").getJSONObject(2);
generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("decoupled_pt_portHopping.eip-service.json"))).getJSONObject(OPENVPN_CONFIGURATION);
- VpnConfigGenerator.Configuration configuration = new VpnConfigGenerator.Configuration();
- configuration.apiVersion = 3;
+ VpnConfigGenerator.Configuration configuration = createProfileConfig(createTransportsFrom(gateway, 3), 3, gateway.optString(IP_ADDRESS), gateway.optString(IP_ADDRESS6), "");
configuration.experimentalTransports = true;
- vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway, configuration);
- HashMap<Connection.TransportType, VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
- assertTrue(vpnProfiles.containsKey(OBFS4_HOP));
- assertTrue(((Obfs4Connection)vpnProfiles.get(OBFS4_HOP).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("tcp"));
- assertFalse(vpnProfiles.containsKey(OPENVPN));
- assertFalse(vpnProfiles.containsKey(OBFS4));
+ vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, configuration);
+ Vector<VpnProfile> vpnProfiles = vpnConfigGenerator.generateVpnProfiles();
+ assertTrue(containsKey(vpnProfiles, OBFS4_HOP));
+ assertTrue(((Obfs4Connection)getVpnProfile(vpnProfiles, OBFS4_HOP).mConnections[0]).getObfs4Options().transport.getProtocols()[0].equals("tcp"));
+ assertFalse(containsKey(vpnProfiles, OPENVPN));
+ assertFalse(containsKey(vpnProfiles, OBFS4));
}
} \ No newline at end of file
diff --git a/app/src/test/java/se/leap/bitmaskclient/pluggableTransports/models/KcpConfigTest.java b/app/src/test/java/se/leap/bitmaskclient/pluggableTransports/models/KcpConfigTest.java
new file mode 100644
index 00000000..9310fd89
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/pluggableTransports/models/KcpConfigTest.java
@@ -0,0 +1,15 @@
+package se.leap.bitmaskclient.pluggableTransports.models;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class KcpConfigTest {
+
+ private static final String KCP_DEFAULTS = "{\"enabled\":true,\"send_window_size\":65535,\"receive_window_size\":65535,\"read_buffer\":16777216,\"write_buffer\":16777216,\"no_delay\":true,\"disable_flow_control\":true,\"interval\":10,\"resend\":2,\"mtu\":1400}";
+ @Test
+ public void testToString() {
+ KcpConfig config = new KcpConfig(true);
+ assertEquals(KCP_DEFAULTS, config.toString());
+ }
+} \ No newline at end of file
diff --git a/app/src/test/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerTest.java
new file mode 100644
index 00000000..adbcf8cb
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerTest.java
@@ -0,0 +1,276 @@
+package se.leap.bitmaskclient.providersetup;
+
+import static org.junit.Assert.*;
+
+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.models.Constants.USE_BRIDGES;
+import static se.leap.bitmaskclient.base.models.Constants.USE_SNOWFLAKE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE;
+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_OK;
+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.testutils.BackendMockResponses.BackendMockProvider.TestBackendErrorCase.NO_ERROR;
+import static se.leap.bitmaskclient.testutils.BackendMockResponses.BackendMockProvider.TestBackendErrorCase.NO_ERROR_API_V4;
+import static se.leap.bitmaskclient.testutils.MockHelper.mockBuildConfigHelper;
+import static se.leap.bitmaskclient.testutils.MockHelper.mockCertificateHelper;
+import static se.leap.bitmaskclient.testutils.MockHelper.mockClientGenerator;
+import static se.leap.bitmaskclient.testutils.MockHelper.mockContext;
+import static se.leap.bitmaskclient.testutils.MockHelper.mockPreferenceHelper;
+import static se.leap.bitmaskclient.testutils.MockHelper.mockProviderApiConnector;
+import static se.leap.bitmaskclient.testutils.MockHelper.mockPrivateKeyHelper;
+import static se.leap.bitmaskclient.testutils.MockHelper.mockResources;
+import static se.leap.bitmaskclient.testutils.MockHelper.mockResultReceiver;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.getConfiguredProvider;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.getConfiguredProviderAPIv4;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.os.Build;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+
+import org.json.JSONException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateEncodingException;
+import java.util.concurrent.TimeoutException;
+
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.base.utils.BuildConfigHelper;
+import se.leap.bitmaskclient.base.utils.CertificateHelper;
+import se.leap.bitmaskclient.base.utils.HandlerProvider;
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import se.leap.bitmaskclient.base.utils.PrivateKeyHelper;
+import se.leap.bitmaskclient.testutils.MockSharedPreferences;
+import se.leap.bitmaskclient.tor.TorStatusObservable;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = {Build.VERSION_CODES.P})
+public class ProviderApiManagerTest {
+
+ private Resources mockResources;
+ private Context mockContext;
+
+ private ProviderApiManager providerApiManager;
+
+ static class TestProviderApiServiceCallback implements ProviderApiManagerBase.ProviderApiServiceCallback {
+ Throwable startTorServiceException;
+ boolean hasNetworkConnection;
+ boolean torTimeout;
+ TorStatusObservable torStatusObservable;
+
+ TestProviderApiServiceCallback() {
+ this(null, true);
+ }
+ TestProviderApiServiceCallback(@Nullable Throwable startTorServiceException, boolean hasNetworkConnection) {
+ this.startTorServiceException = startTorServiceException;
+ this.hasNetworkConnection = hasNetworkConnection;
+ this.torStatusObservable = TorStatusObservable.getInstance();
+ }
+
+ TestProviderApiServiceCallback(boolean torTimeout, boolean hasNetworkConnection) {
+ this.hasNetworkConnection = hasNetworkConnection;
+ this.torStatusObservable = TorStatusObservable.getInstance();
+ this.torTimeout = torTimeout;
+ }
+
+ @Override
+ public void broadcastEvent(Intent intent) {
+ }
+
+ @Override
+ public boolean startTorService() throws InterruptedException, IllegalStateException {
+ if (startTorServiceException != null) {
+ if (startTorServiceException instanceof InterruptedException) {
+ throw (InterruptedException) startTorServiceException;
+ }
+ if (startTorServiceException instanceof IllegalStateException) {
+ throw (IllegalStateException) startTorServiceException;
+ }
+ }
+ if (!torTimeout) {
+ try {
+ TorStatusObservable.updateState(mockContext(), TorStatusObservable.TorStatus.ON.toString());
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void stopTorService() {
+ }
+
+ @Override
+ public int getTorHttpTunnelPort() {
+ return 8118;
+ }
+
+ @Override
+ public int getTorSocksProxyPort() {
+ return 9050;
+ }
+
+ @Override
+ public boolean hasNetworkConnection() {
+ return hasNetworkConnection;
+ }
+
+ @Override
+ public void saveProvider(Provider p) {
+
+ }
+
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ mockContext = mockContext();
+ mockResources = mockResources(getClass().getClassLoader().getResourceAsStream("error_messages.json"));
+
+ HandlerProvider handlerProvider = new HandlerProvider((r, delay) -> new Thread(r).start());
+ BuildConfigHelper buildConfigHelper = mockBuildConfigHelper(true);
+ TorStatusObservable torStatusObservable = TorStatusObservable.getInstance();
+ TorStatusObservable.setProxyPort(-1);
+ TorStatusObservable.setLastError(null);
+ TorStatusObservable.updateState(mockContext, TorStatusObservable.TorStatus.OFF.toString());
+ }
+
+ @Test
+ public void test_handleIntentSetupProvider_TorBridgesPreferencesEnabledTimeout_TimeoutError() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, TimeoutException, InterruptedException {
+ Provider provider = getConfiguredProviderAPIv4();
+ SharedPreferences sharedPreferences = new MockSharedPreferences();
+ sharedPreferences.edit().putBoolean(USE_BRIDGES, true).putBoolean(USE_SNOWFLAKE, true).commit();
+ PreferenceHelper preferenceHelper = mockPreferenceHelper(provider, sharedPreferences);
+ ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR_API_V4);
+
+ providerApiManager = new ProviderApiManager(mockResources, new ProviderApiManagerTest.TestProviderApiServiceCallback(true, true));
+
+ Bundle expectedResult = new Bundle();
+ expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
+ expectedResult.putString(ERRORS, "{\"errors\":\"Starting bridges failed. Do you want to retry or continue with an unobfuscated secure connection to configure Bitmask?\",\"errorId\":\"ERROR_TOR_TIMEOUT\",\"initalAction\":\"setUpProvider\"}");
+ expectedResult.putParcelable(PROVIDER_KEY, provider);
+
+ Intent providerApiCommand = new Intent();
+ providerApiCommand.putExtra(PROVIDER_KEY, provider);
+ providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
+ providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(TOR_TIMEOUT, expectedResult));
+ providerApiCommand.putExtra(PARAMETERS, new Bundle());
+
+ providerApiManager.handleIntent(providerApiCommand);
+ //providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(TOR_TIMEOUT, expectedResult));
+ assertEquals(-1, TorStatusObservable.getProxyPort());
+ }
+
+ @Test
+ public void test_handleIntentSetupProvider_noNetwork_NetworkError() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, JSONException {
+ Provider provider = getConfiguredProvider();
+
+ CertificateHelper certHelper = mockCertificateHelper("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+ ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR);
+ providerApiManager = new ProviderApiManager(mockResources, new ProviderApiManagerTest.TestProviderApiServiceCallback(null, false));
+ Bundle expectedResult = new Bundle();
+
+ expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
+ expectedResult.putString(ERRORS, "{\"errors\":\"Bitmask has no internet connection. Please check your WiFi and cellular data settings.\"}");
+ expectedResult.putParcelable(PROVIDER_KEY, provider);
+
+ Intent providerApiCommand = new Intent();
+
+ providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
+ providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(MISSING_NETWORK_CONNECTION, expectedResult));
+ providerApiCommand.putExtra(PROVIDER_KEY, provider);
+ providerApiCommand.putExtra(PARAMETERS, new Bundle());
+ providerApiManager.handleIntent(providerApiCommand);
+ }
+
+
+ @Test
+ public void test_handleIntentSetupProvider_TorBridgesPreferenceEnabled_Success() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, TimeoutException, InterruptedException {
+ Provider provider = getConfiguredProviderAPIv4();
+
+ SharedPreferences sharedPreferences = new MockSharedPreferences();
+ sharedPreferences.edit().putBoolean(USE_BRIDGES, true).putBoolean(USE_SNOWFLAKE, true).commit();
+ PreferenceHelper preferenceHelper = mockPreferenceHelper(provider, sharedPreferences);
+ CertificateHelper certHelper = mockCertificateHelper(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+ ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR_API_V4);
+
+ providerApiManager = new ProviderApiManager(mockResources, new ProviderApiManagerTest.TestProviderApiServiceCallback());
+
+ Intent providerApiCommand = new Intent();
+ providerApiCommand.putExtra(PROVIDER_KEY, provider);
+ providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
+ providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK));
+ providerApiCommand.putExtra(PARAMETERS, new Bundle());
+
+ providerApiManager.handleIntent(providerApiCommand);
+ assertEquals(8118, TorStatusObservable.getProxyPort());
+ }
+
+
+ @Test
+ public void test_handleIntentUpdateVPNCertificate_TorBridgesPreferencesTrue_TorStartedAndSuccess() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, TimeoutException, InterruptedException {
+ Provider provider = getConfiguredProviderAPIv4();
+
+ SharedPreferences sharedPreferences = new MockSharedPreferences();
+ sharedPreferences.edit().putBoolean(USE_BRIDGES, true).putBoolean(USE_SNOWFLAKE, true).commit();
+ PreferenceHelper preferenceHelper = mockPreferenceHelper(provider, sharedPreferences);
+ CertificateHelper certHelper = mockCertificateHelper(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+ PrivateKeyHelper privateKeyHelper = mockPrivateKeyHelper();
+ ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR_API_V4);
+
+ providerApiManager = new ProviderApiManager(mockResources, new ProviderApiManagerTest.TestProviderApiServiceCallback());
+
+ Intent providerApiCommand = new Intent();
+ providerApiCommand.putExtra(PROVIDER_KEY, provider);
+ providerApiCommand.setAction(UPDATE_INVALID_VPN_CERTIFICATE);
+ providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE));
+ providerApiCommand.putExtra(PARAMETERS, new Bundle());
+
+ providerApiManager.handleIntent(providerApiCommand);
+ assertNotEquals(-1, TorStatusObservable.getProxyPort());
+ }
+
+ @Test
+ public void test_handleIntentUpdateVPNCertificate_TorBridgesPreferencesTrue_TorException_Failure() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, TimeoutException, InterruptedException {
+ Provider provider = getConfiguredProviderAPIv4();
+
+ SharedPreferences sharedPreferences = new MockSharedPreferences();
+ sharedPreferences.edit().putBoolean(USE_BRIDGES, true).putBoolean(USE_SNOWFLAKE, true).commit();
+ PreferenceHelper preferenceHelper = mockPreferenceHelper(provider, sharedPreferences);
+ CertificateHelper certHelper = mockCertificateHelper(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+ ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR_API_V4);
+
+ providerApiManager = new ProviderApiManager(mockResources, new ProviderApiManagerTest.TestProviderApiServiceCallback(new IllegalStateException("Nothing works always."), true));
+
+ Bundle expectedResult = new Bundle();
+ expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
+ expectedResult.putString(ERRORS, "{\"initalAction\":\"ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE\"}");
+ expectedResult.putParcelable(PROVIDER_KEY, provider);
+
+ Intent providerApiCommand = new Intent();
+ providerApiCommand.putExtra(PROVIDER_KEY, provider);
+ providerApiCommand.setAction(UPDATE_INVALID_VPN_CERTIFICATE);
+ providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(TOR_EXCEPTION, expectedResult));
+ providerApiCommand.putExtra(PARAMETERS, new Bundle());
+
+ providerApiManager.handleIntent(providerApiCommand);
+ assertEquals(-1, TorStatusObservable.getProxyPort());
+ }
+} \ No newline at end of file
diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerV3Test.java
index b8c6d0c9..5751d1c5 100644
--- a/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerV3Test.java
@@ -15,7 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-package se.leap.bitmaskclient.eip;
+package se.leap.bitmaskclient.providersetup;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
@@ -26,15 +26,14 @@ import static se.leap.bitmaskclient.base.models.Constants.USE_BRIDGES;
import static se.leap.bitmaskclient.base.models.Constants.USE_SNOWFLAKE;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_GEOIP_JSON;
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.ERRORS;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_GEOIP_JSON;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE;
-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.TOR_EXCEPTION;
-import static se.leap.bitmaskclient.providersetup.ProviderAPI.TOR_TIMEOUT;
+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.testutils.BackendMockResponses.BackendMockProvider.TestBackendErrorCase.ERROR_CASE_FETCH_EIP_SERVICE_CERTIFICATE_INVALID;
import static se.leap.bitmaskclient.testutils.BackendMockResponses.BackendMockProvider.TestBackendErrorCase.ERROR_CASE_MICONFIGURED_PROVIDER;
import static se.leap.bitmaskclient.testutils.BackendMockResponses.BackendMockProvider.TestBackendErrorCase.ERROR_CASE_UPDATED_CERTIFICATE;
@@ -48,8 +47,8 @@ import static se.leap.bitmaskclient.testutils.MockHelper.mockCertificateHelper;
import static se.leap.bitmaskclient.testutils.MockHelper.mockClientGenerator;
import static se.leap.bitmaskclient.testutils.MockHelper.mockContext;
import static se.leap.bitmaskclient.testutils.MockHelper.mockPreferenceHelper;
+import static se.leap.bitmaskclient.testutils.MockHelper.mockPrivateKeyHelper;
import static se.leap.bitmaskclient.testutils.MockHelper.mockProviderApiConnector;
-import static se.leap.bitmaskclient.testutils.MockHelper.mockRSAHelper;
import static se.leap.bitmaskclient.testutils.MockHelper.mockResources;
import static se.leap.bitmaskclient.testutils.MockHelper.mockResultReceiver;
import static se.leap.bitmaskclient.testutils.TestSetupHelper.getConfiguredProvider;
@@ -61,6 +60,7 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Resources;
+import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Nullable;
@@ -69,23 +69,21 @@ import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.util.concurrent.TimeoutException;
-import se.leap.bitmaskclient.BuildConfig;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.base.utils.BuildConfigHelper;
import se.leap.bitmaskclient.base.utils.CertificateHelper;
import se.leap.bitmaskclient.base.utils.HandlerProvider;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
-import se.leap.bitmaskclient.base.utils.RSAHelper;
-import se.leap.bitmaskclient.providersetup.ProviderAPI;
-import se.leap.bitmaskclient.providersetup.ProviderApiConnector;
-import se.leap.bitmaskclient.providersetup.ProviderApiManager;
-import se.leap.bitmaskclient.providersetup.ProviderApiManagerBase;
+import se.leap.bitmaskclient.base.utils.PrivateKeyHelper;
import se.leap.bitmaskclient.testutils.MockSharedPreferences;
import se.leap.bitmaskclient.tor.TorStatusObservable;
@@ -93,13 +91,14 @@ import se.leap.bitmaskclient.tor.TorStatusObservable;
/**
* Created by cyberta on 04.01.18.
*/
-
-public class ProviderApiManagerTest {
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = {Build.VERSION_CODES.P})
+public class ProviderApiManagerV3Test {
private Resources mockResources;
private Context mockContext;
- private ProviderApiManager providerApiManager;
+ private ProviderApiManagerV3 providerApiManager;
static class TestProviderApiServiceCallback implements ProviderApiManagerBase.ProviderApiServiceCallback {
Throwable startTorServiceException;
@@ -156,6 +155,11 @@ public class ProviderApiManagerTest {
}
@Override
+ public int getTorSocksProxyPort() {
+ return 9050;
+ }
+
+ @Override
public boolean hasNetworkConnection() {
return hasNetworkConnection;
}
@@ -171,6 +175,7 @@ public class ProviderApiManagerTest {
public void setUp() throws Exception {
mockContext = mockContext();
mockResources = mockResources(getClass().getClassLoader().getResourceAsStream("error_messages.json"));
+
HandlerProvider handlerProvider = new HandlerProvider((r, delay) -> new Thread(r).start());
BuildConfigHelper buildConfigHelper = mockBuildConfigHelper(true);
TorStatusObservable torStatusObservable = TorStatusObservable.getInstance();
@@ -183,20 +188,14 @@ public class ProviderApiManagerTest {
public void test_handleIntentSetupProvider_noProviderMainURL() throws IOException, JSONException {
Provider provider = new Provider("");
PreferenceHelper preferenceHelper = mockPreferenceHelper(provider);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
expectedResult.putString(ERRORS, "{\"errors\":\"It doesn't seem to be a Bitmask provider.\"}");
expectedResult.putParcelable(PROVIDER_KEY, provider);
- Intent providerApiCommand = new Intent();
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_NOK, expectedResult));
}
@Test
@@ -206,17 +205,12 @@ public class ProviderApiManagerTest {
CertificateHelper certHelper = mockCertificateHelper(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
ProviderApiConnector providerApiConnector = mockProviderApiConnector(NO_ERROR);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(BROADCAST_RESULT_KEY, true);
expectedResult.putParcelable(PROVIDER_KEY, provider);
- Intent providerApiCommand = new Intent();
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK, expectedResult));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_OK, expectedResult));
}
@Test
@@ -226,20 +220,13 @@ public class ProviderApiManagerTest {
CertificateHelper certHelper = mockCertificateHelper("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(BROADCAST_RESULT_KEY, true);
expectedResult.putParcelable(PROVIDER_KEY, provider);
- Intent providerApiCommand = new Intent();
-
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK, expectedResult));
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_OK, expectedResult));
}
@Test
@@ -249,19 +236,13 @@ public class ProviderApiManagerTest {
CertificateHelper certHelper = mockCertificateHelper("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(BROADCAST_RESULT_KEY, true);
expectedResult.putParcelable(PROVIDER_KEY, provider);
- Intent providerApiCommand = new Intent();
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK, expectedResult));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_OK, expectedResult));
}
@Test
@@ -271,19 +252,13 @@ public class ProviderApiManagerTest {
PreferenceHelper preferenceHelper = mockPreferenceHelper(provider);
CertificateHelper certHelper = mockCertificateHelper(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29495");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
- expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_CERTIFICATE_PINNING\",\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}");
+ expectedResult.putString(ERRORS, "{\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\",\"errorId\":\"ERROR_CERTIFICATE_PINNING\"}");
expectedResult.putParcelable(PROVIDER_KEY, provider);
- Intent providerApiCommand = new Intent();
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_NOK, expectedResult));
}
@Test
@@ -293,20 +268,14 @@ public class ProviderApiManagerTest {
CertificateHelper certHelper = mockCertificateHelper("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29495");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
- expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_CERTIFICATE_PINNING\",\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}");
+ expectedResult.putString(ERRORS, "{\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\",\"errorId\":\"ERROR_CERTIFICATE_PINNING\"}");
expectedResult.putParcelable(PROVIDER_KEY, provider);
- Intent providerApiCommand = new Intent();
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_NOK, expectedResult));
}
@Test
@@ -316,20 +285,14 @@ public class ProviderApiManagerTest {
CertificateHelper certHelper = mockCertificateHelper("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29495");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
- expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_CERTIFICATE_PINNING\",\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}");
+ expectedResult.putString(ERRORS, "{\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\",\"errorId\":\"ERROR_CERTIFICATE_PINNING\"}");
expectedResult.putParcelable(PROVIDER_KEY, provider);
- Intent providerApiCommand = new Intent();
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_NOK, expectedResult));
}
@@ -339,20 +302,14 @@ public class ProviderApiManagerTest {
PreferenceHelper preferenceHelper = mockPreferenceHelper(provider);
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR);
CertificateHelper certHelper = mockCertificateHelper("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
- expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_INVALID_CERTIFICATE\",\"errors\":\"Stored provider certificate is expired. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}");
+ expectedResult.putString(ERRORS, "{\"errors\":\"Stored provider certificate is expired. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\",\"errorId\":\"ERROR_INVALID_CERTIFICATE\"}");
expectedResult.putParcelable(PROVIDER_KEY, provider);
- Intent providerApiCommand = new Intent();
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_NOK, expectedResult));
}
@Test
@@ -362,20 +319,14 @@ public class ProviderApiManagerTest {
PreferenceHelper.getEipDefinitionFromPreferences();
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR);
CertificateHelper certHelper = mockCertificateHelper("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
- expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_INVALID_CERTIFICATE\",\"errors\":\"Stored provider certificate is expired. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}");
+ expectedResult.putString(ERRORS, "{\"errors\":\"Stored provider certificate is expired. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\",\"errorId\":\"ERROR_INVALID_CERTIFICATE\"}");
expectedResult.putParcelable(PROVIDER_KEY, provider);
- Intent providerApiCommand = new Intent();
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_NOK, expectedResult));
}
@Test
@@ -385,99 +336,70 @@ public class ProviderApiManagerTest {
CertificateHelper certHelper = mockCertificateHelper("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(ERROR_CASE_UPDATED_CERTIFICATE);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
- expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_INVALID_CERTIFICATE\",\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}");
+ expectedResult.putString(ERRORS, "{\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\",\"errorId\":\"ERROR_INVALID_CERTIFICATE\"}");
expectedResult.putParcelable(PROVIDER_KEY, provider);
Intent providerApiCommand = new Intent();
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_NOK, expectedResult));
}
@Test
public void test_handleIntentSetupProvider_storedProviderAndCAFromPreviousSetup_ValidCertificateButUpdatedCertificateOnServerSide() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, JSONException {
- Provider provider = new Provider("https://riseup.net");
- PreferenceHelper preferenceHelper = mockPreferenceHelper(getConfiguredProvider());
+ Provider provider = getConfiguredProvider();
+ PreferenceHelper preferenceHelper = mockPreferenceHelper(provider);
CertificateHelper certHelper = mockCertificateHelper("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(ERROR_CASE_UPDATED_CERTIFICATE);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
- expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_INVALID_CERTIFICATE\",\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}");
+ expectedResult.putString(ERRORS, "{\"errors\":\"Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\",\"errorId\":\"ERROR_INVALID_CERTIFICATE\"}");
expectedResult.putParcelable(PROVIDER_KEY, provider);
- Intent providerApiCommand = new Intent();
-
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_NOK, expectedResult));
}
@Test
public void test_handleIntentSetupProvider_preseededProviderAndCA_failedConfiguration() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, JSONException {
- Provider provider = getConfiguredProvider();
+ Provider provider = getProvider(null, null, null, null, null, "riseup_net_invalid_config.json", null, null);
PreferenceHelper preferenceHelper = mockPreferenceHelper(provider);
CertificateHelper certHelper = mockCertificateHelper("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
- ProviderApiConnector mockedApiConnector = mockProviderApiConnector(ERROR_CASE_MICONFIGURED_PROVIDER);
+ ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
expectedResult.putString(ERRORS, "{\"errors\":\"There was an error configuring Bitmask with your chosen provider.\"}");
expectedResult.putParcelable(PROVIDER_KEY, provider);
- Intent providerApiCommand = new Intent();
-
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_NOK, expectedResult));
}
@Test
public void test_handleIntentSetupProvider_preseededCustomProviderAndCA_failedConfiguration() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, JSONException {
- if ("insecure".equals(BuildConfig.FLAVOR_implementation )) {
- return;
- }
- Provider provider = getConfiguredProvider();
- PreferenceHelper preferenceHelper = mockPreferenceHelper(provider);
+ Provider provider = new Provider("riseup.net");
+ PreferenceHelper preferenceHelper = new PreferenceHelper(new MockSharedPreferences());
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(ERROR_CASE_MICONFIGURED_PROVIDER);
CertificateHelper certHelper = mockCertificateHelper("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
BuildConfigHelper buildConfigHelper = mockBuildConfigHelper(false);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
expectedResult.putString(ERRORS, "{\"errors\":\"There was an error configuring RiseupVPN.\"}");
expectedResult.putParcelable(PROVIDER_KEY, provider);
- Intent providerApiCommand = new Intent();
-
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_NOK, expectedResult));
}
@Test
@@ -488,88 +410,56 @@ public class ProviderApiManagerTest {
CertificateHelper certHelper = mockCertificateHelper(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(BROADCAST_RESULT_KEY, true);
expectedResult.putParcelable(PROVIDER_KEY, provider);
- Intent providerApiCommand = new Intent();
-
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK, expectedResult));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_OK, expectedResult));
}
@Test
public void test_handleIntentSetupProvider_failingEipServiceFetch_failedConfiguration() throws IOException, NoSuchAlgorithmException, CertificateEncodingException {
- if ("insecure".equals(BuildConfig.FLAVOR_implementation )) {
- return;
- }
-
Provider provider = new Provider("https://riseup.net");
PreferenceHelper preferenceHelper = mockPreferenceHelper(provider);
CertificateHelper certHelper = mockCertificateHelper("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(ERROR_CASE_FETCH_EIP_SERVICE_CERTIFICATE_INVALID);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
expectedResult.putParcelable(PROVIDER_KEY, provider);
expectedResult.putString(ERRORS, "This is not a trusted Bitmask provider.");
- Intent providerApiCommand = new Intent();
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_NOK, expectedResult));
}
@Test
public void test_handleIntentGetGeoip_happyPath() throws IOException, NoSuchAlgorithmException, CertificateEncodingException, JSONException {
- if ("insecure".equals(BuildConfig.FLAVOR_implementation )) {
- return;
- }
-
- Provider inputProvider = getConfiguredProvider();
- inputProvider.setGeoIpJson(new JSONObject());
- PreferenceHelper preferenceHelper = mockPreferenceHelper(inputProvider);
+ Provider provider = getConfiguredProvider();
+ provider.setGeoIpJson(new JSONObject());
+ PreferenceHelper preferenceHelper = mockPreferenceHelper(provider);
Provider expectedProvider = getConfiguredProvider();
CertificateHelper certHelper = mockCertificateHelper("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(EIP_ACTION_START, true);
expectedResult.putBoolean(BROADCAST_RESULT_KEY, true);
expectedResult.putParcelable(PROVIDER_KEY, expectedProvider);
- Intent providerApiCommand = new Intent();
-
- providerApiCommand.setAction(ProviderAPI.DOWNLOAD_GEOIP_JSON);
Bundle extrasBundle = new Bundle();
extrasBundle.putBoolean(EIP_ACTION_START, true);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(CORRECTLY_DOWNLOADED_GEOIP_JSON, expectedResult));
- providerApiCommand.putExtra(PROVIDER_KEY, inputProvider);
- providerApiCommand.putExtra(PARAMETERS, extrasBundle);
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(DOWNLOAD_GEOIP_JSON, provider, extrasBundle, mockResultReceiver(CORRECTLY_DOWNLOADED_GEOIP_JSON, expectedResult));
}
@Test
public void test_handleIntentGetGeoip_serviceDown_failToDownload() throws IOException, NoSuchAlgorithmException, CertificateEncodingException, JSONException {
- if ("insecure".equals(BuildConfig.FLAVOR_implementation)) {
- return;
- }
-
Provider provider = getConfiguredProvider();
SharedPreferences mockSharedPref = new MockSharedPreferences();
mockSharedPref.edit().
@@ -578,53 +468,35 @@ public class ProviderApiManagerTest {
PreferenceHelper preferenceHelper = mockPreferenceHelper(provider, mockSharedPref);
CertificateHelper certHelper = mockCertificateHelper("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(ERROR_GEOIP_SERVICE_IS_DOWN);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(EIP_ACTION_START, true);
expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
expectedResult.putParcelable(PROVIDER_KEY, provider);
- Intent providerApiCommand = new Intent();
-
- providerApiCommand.setAction(ProviderAPI.DOWNLOAD_GEOIP_JSON);
Bundle extrasBundle = new Bundle();
extrasBundle.putBoolean(EIP_ACTION_START, true);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(INCORRECTLY_DOWNLOADED_GEOIP_JSON, expectedResult));
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.putExtra(PARAMETERS, extrasBundle);
-
- providerApiManager.handleIntent(providerApiCommand);
-
+ providerApiManager.handleAction(DOWNLOAD_GEOIP_JSON, provider, extrasBundle, mockResultReceiver(INCORRECTLY_DOWNLOADED_GEOIP_JSON, expectedResult));
}
@Test
public void test_handleIntentGetGeoip_serviceDown_torNotStarted() throws IOException, NoSuchAlgorithmException, CertificateEncodingException, JSONException, TimeoutException, InterruptedException {
- if ("insecure".equals(BuildConfig.FLAVOR_implementation)) {
- return;
- }
-
Provider provider = getConfiguredProvider();
PreferenceHelper preferenceHelper = mockPreferenceHelper(provider);
CertificateHelper certHelper = mockCertificateHelper("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(ERROR_GEOIP_SERVICE_IS_DOWN_TOR_FALLBACK);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(EIP_ACTION_START, true);
expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
expectedResult.putParcelable(PROVIDER_KEY, provider);
- Intent providerApiCommand = new Intent();
-
- providerApiCommand.setAction(ProviderAPI.DOWNLOAD_GEOIP_JSON);
Bundle extrasBundle = new Bundle();
extrasBundle.putBoolean(EIP_ACTION_START, true);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(INCORRECTLY_DOWNLOADED_GEOIP_JSON, expectedResult));
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.putExtra(PARAMETERS, extrasBundle);
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(DOWNLOAD_GEOIP_JSON, provider, extrasBundle, mockResultReceiver(INCORRECTLY_DOWNLOADED_GEOIP_JSON, expectedResult));
+
// also assert that Tor was not allowed to start
assertEquals(-1, TorStatusObservable.getProxyPort());
@@ -632,39 +504,26 @@ public class ProviderApiManagerTest {
@Test
public void test_handleIntentGetGeoip_didNotReachTimeoutToFetchNew_returnsFailure() throws IOException, NoSuchAlgorithmException, CertificateEncodingException, JSONException {
- if ("insecure".equals(BuildConfig.FLAVOR_implementation)) {
- return;
- }
-
Provider provider = getConfiguredProvider();
provider.setLastGeoIpUpdate(System.currentTimeMillis());
PreferenceHelper preferenceHelper = mockPreferenceHelper(provider);
CertificateHelper certHelper = mockCertificateHelper("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(EIP_ACTION_START, true);
expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
expectedResult.putParcelable(PROVIDER_KEY, provider);
- Intent providerApiCommand = new Intent();
-
- providerApiCommand.setAction(ProviderAPI.DOWNLOAD_GEOIP_JSON);
Bundle extrasBundle = new Bundle();
extrasBundle.putBoolean(EIP_ACTION_START, true);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(INCORRECTLY_DOWNLOADED_GEOIP_JSON, expectedResult));
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.putExtra(PARAMETERS, extrasBundle);
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(DOWNLOAD_GEOIP_JSON, provider, extrasBundle, mockResultReceiver(INCORRECTLY_DOWNLOADED_GEOIP_JSON, expectedResult));
+
}
@Test
public void test_handleIntentGetGeoip_noGeoipServiceURLDefined_returnsFailure() throws IOException, NoSuchAlgorithmException, CertificateEncodingException, JSONException {
- if ("insecure".equals(BuildConfig.FLAVOR_implementation)) {
- return;
- }
Provider provider = getConfiguredProvider();
provider.setGeoipUrl(null);
@@ -672,23 +531,14 @@ public class ProviderApiManagerTest {
PreferenceHelper preferenceHelper = mockPreferenceHelper(provider);
CertificateHelper certHelper = mockCertificateHelper("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(EIP_ACTION_START, true);
expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
expectedResult.putParcelable(PROVIDER_KEY, provider);
- Intent providerApiCommand = new Intent();
-
- providerApiCommand.setAction(ProviderAPI.DOWNLOAD_GEOIP_JSON);
- Bundle extrasBundle = new Bundle();
- extrasBundle.putBoolean(EIP_ACTION_START, true);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(INCORRECTLY_DOWNLOADED_GEOIP_JSON, expectedResult));
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.putExtra(PARAMETERS, extrasBundle);
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(DOWNLOAD_GEOIP_JSON, provider, new Bundle(), mockResultReceiver(CORRECTLY_DOWNLOADED_GEOIP_JSON, expectedResult));
}
@Test
@@ -698,36 +548,24 @@ public class ProviderApiManagerTest {
CertificateHelper certHelper = mockCertificateHelper(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR_API_V4);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = new Bundle();
expectedResult.putBoolean(BROADCAST_RESULT_KEY, true);
expectedResult.putParcelable(PROVIDER_KEY, provider);
- Intent providerApiCommand = new Intent();
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK, expectedResult));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_OK, expectedResult));
}
@Test
public void test_handleIntentSetupProvider_TorFallback_SecondTryHappyPath() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, TimeoutException, InterruptedException {
- Provider provider = getConfiguredProviderAPIv4();
+ Provider provider = new Provider("riseup.net");
PreferenceHelper preferenceHelper = mockPreferenceHelper(provider);
CertificateHelper certHelper = mockCertificateHelper(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(ERROR_DNS_RESUOLUTION_TOR_FALLBACK);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
- Intent providerApiCommand = new Intent();
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_OK));
assertNotEquals(-1, TorStatusObservable.getProxyPort());
}
@@ -738,15 +576,9 @@ public class ProviderApiManagerTest {
PreferenceHelper preferenceHelper = mockPreferenceHelper(provider);
CertificateHelper certHelper = mockCertificateHelper(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(ERROR_DNS_RESUOLUTION_TOR_FALLBACK);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback(new IllegalStateException("Tor service start not failed."), true));
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback(new IllegalStateException("Tor service start not failed."), true));
- Intent providerApiCommand = new Intent();
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_NOK));
assertEquals(-1, TorStatusObservable.getProxyPort());
}
@@ -756,41 +588,13 @@ public class ProviderApiManagerTest {
PreferenceHelper preferenceHelper = mockPreferenceHelper(provider);
CertificateHelper certHelper = mockCertificateHelper(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(ERROR_DNS_RESUOLUTION_TOR_FALLBACK);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback(true, true));
-
- Intent providerApiCommand = new Intent();
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback(true, true));
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_NOK));
assertEquals(-1, TorStatusObservable.getProxyPort());
}
@Test
- public void test_handleIntentSetupProvider_TorBridgesPreferenceEnabled_Success() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, TimeoutException, InterruptedException {
- Provider provider = getConfiguredProviderAPIv4();
-
- SharedPreferences sharedPreferences = new MockSharedPreferences();
- sharedPreferences.edit().putBoolean(USE_BRIDGES, true).putBoolean(USE_SNOWFLAKE, true).commit();
- PreferenceHelper preferenceHelper = mockPreferenceHelper(provider, sharedPreferences);
- CertificateHelper certHelper = mockCertificateHelper(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
- ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR_API_V4);
-
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
-
- Intent providerApiCommand = new Intent();
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
-
- providerApiManager.handleIntent(providerApiCommand);
- assertEquals(8118, TorStatusObservable.getProxyPort());
- }
-
- @Test
public void test_handleIntentSetupProvider_TorBridgesDisabled_TorNotStarted() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, TimeoutException, InterruptedException {
Provider provider = getConfiguredProviderAPIv4();
@@ -800,15 +604,9 @@ public class ProviderApiManagerTest {
CertificateHelper certHelper = mockCertificateHelper(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR_API_V4);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
- Intent providerApiCommand = new Intent();
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(SET_UP_PROVIDER, provider, new Bundle(), mockResultReceiver(PROVIDER_OK));
assertEquals(-1, TorStatusObservable.getProxyPort());
}
@@ -817,18 +615,12 @@ public class ProviderApiManagerTest {
Provider provider = getConfiguredProviderAPIv4();
PreferenceHelper preferenceHelper = mockPreferenceHelper(provider);
CertificateHelper certHelper = mockCertificateHelper(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
- RSAHelper rsaHelper = mockRSAHelper();
+ PrivateKeyHelper privateKeyHelper = mockPrivateKeyHelper();
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(ERROR_DNS_RESUOLUTION_TOR_FALLBACK);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
- Intent providerApiCommand = new Intent();
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.setAction(ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(UPDATE_INVALID_VPN_CERTIFICATE, provider, new Bundle(), mockResultReceiver(CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE));
assertEquals(8118, TorStatusObservable.getProxyPort());
}
@@ -842,112 +634,9 @@ public class ProviderApiManagerTest {
CertificateHelper certHelper = mockCertificateHelper(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
ProviderApiConnector mockedApiConnector = mockProviderApiConnector(ERROR_DNS_RESUOLUTION_TOR_FALLBACK);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ providerApiManager = new ProviderApiManagerV3(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
- Intent providerApiCommand = new Intent();
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.setAction(ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
-
- providerApiManager.handleIntent(providerApiCommand);
+ providerApiManager.handleAction(UPDATE_INVALID_VPN_CERTIFICATE, provider, new Bundle(), mockResultReceiver(INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE));
assertEquals(-1, TorStatusObservable.getProxyPort());
}
-
- @Test
- public void test_handleIntentUpdateVPNCertificate_TorBridgesPreferencesTrue_TorStartedAndSuccess() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, TimeoutException, InterruptedException {
- Provider provider = getConfiguredProviderAPIv4();
-
- SharedPreferences sharedPreferences = new MockSharedPreferences();
- sharedPreferences.edit().putBoolean(USE_BRIDGES, true).putBoolean(USE_SNOWFLAKE, true).commit();
- PreferenceHelper preferenceHelper = mockPreferenceHelper(provider, sharedPreferences);
- CertificateHelper certHelper = mockCertificateHelper(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
- RSAHelper rsaHelper = mockRSAHelper();
- ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR_API_V4);
-
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
-
- Intent providerApiCommand = new Intent();
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.setAction(ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
-
- providerApiManager.handleIntent(providerApiCommand);
- assertNotEquals(-1, TorStatusObservable.getProxyPort());
- }
-
- @Test
- public void test_handleIntentUpdateVPNCertificate_TorBridgesPreferencesTrue_TorException_Failure() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, TimeoutException, InterruptedException {
- Provider provider = getConfiguredProviderAPIv4();
-
- SharedPreferences sharedPreferences = new MockSharedPreferences();
- sharedPreferences.edit().putBoolean(USE_BRIDGES, true).putBoolean(USE_SNOWFLAKE, true).commit();
- PreferenceHelper preferenceHelper = mockPreferenceHelper(provider, sharedPreferences);
- CertificateHelper certHelper = mockCertificateHelper(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
- ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR_API_V4);
-
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback(new IllegalStateException("Nothing works always."), true));
-
- Bundle expectedResult = new Bundle();
- expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
- expectedResult.putString(ERRORS, "{\"initalAction\":\"ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE\"}");
- expectedResult.putParcelable(PROVIDER_KEY, provider);
-
- Intent providerApiCommand = new Intent();
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.setAction(ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(TOR_EXCEPTION, expectedResult));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
-
- providerApiManager.handleIntent(providerApiCommand);
- assertEquals(-1, TorStatusObservable.getProxyPort());
- }
-
- @Test
- public void test_handleIntentSetupProvider_TorBridgesPreferencesEnabledTimeout_TimeoutError() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, TimeoutException, InterruptedException {
- Provider provider = getConfiguredProviderAPIv4();
- SharedPreferences sharedPreferences = new MockSharedPreferences();
- sharedPreferences.edit().putBoolean(USE_BRIDGES, true).putBoolean(USE_SNOWFLAKE, true).commit();
- PreferenceHelper preferenceHelper = mockPreferenceHelper(provider, sharedPreferences);
- ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR_API_V4);
-
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback(true, true));
-
- Bundle expectedResult = new Bundle();
- expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
- expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_TOR_TIMEOUT\",\"initalAction\":\"setUpProvider\",\"errors\":\"Starting bridges failed. Do you want to retry or continue with an unobfuscated secure connection to configure Bitmask?\"}");
- expectedResult.putParcelable(PROVIDER_KEY, provider);
-
- Intent providerApiCommand = new Intent();
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(TOR_TIMEOUT, expectedResult));
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
-
- providerApiManager.handleIntent(providerApiCommand);
- assertEquals(-1, TorStatusObservable.getProxyPort());
- }
-
- @Test
- public void test_handleIntentSetupProvider_noNetwork_NetworkError() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, JSONException {
- Provider provider = getConfiguredProvider();
-
- CertificateHelper certHelper = mockCertificateHelper("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
- ProviderApiConnector mockedApiConnector = mockProviderApiConnector(NO_ERROR);
- providerApiManager = new ProviderApiManager(mockResources, mockClientGenerator(), new TestProviderApiServiceCallback(null, false));
- Bundle expectedResult = new Bundle();
-
- expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
- expectedResult.putString(ERRORS, "{\"errors\":\"Bitmask has no internet connection. Please check your WiFi and cellular data settings.\"}");
- expectedResult.putParcelable(PROVIDER_KEY, provider);
-
- Intent providerApiCommand = new Intent();
-
- providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
- providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(MISSING_NETWORK_CONNECTION, expectedResult));
- providerApiCommand.putExtra(PROVIDER_KEY, provider);
- providerApiCommand.putExtra(PARAMETERS, new Bundle());
- providerApiManager.handleIntent(providerApiCommand);
- }
}
diff --git a/app/src/test/java/se/leap/bitmaskclient/providersetup/ProviderManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/providersetup/ProviderManagerTest.java
index 925d2464..b6a2becd 100644
--- a/app/src/test/java/se/leap/bitmaskclient/providersetup/ProviderManagerTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/providersetup/ProviderManagerTest.java
@@ -9,26 +9,36 @@ import static org.mockito.Mockito.when;
import android.content.SharedPreferences;
import android.content.res.AssetManager;
+import android.os.Build;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Set;
+import mobile.BitmaskMobile;
+import mobilemodels.BitmaskMobileCore;
import se.leap.bitmaskclient.base.models.Constants;
import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.base.utils.BitmaskCoreProvider;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
import se.leap.bitmaskclient.testutils.MockHelper;
import se.leap.bitmaskclient.testutils.MockSharedPreferences;
+import se.leap.bitmaskclient.testutils.TestSetupHelper;
/**
* Created by cyberta on 20.02.18.
*/
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = {Build.VERSION_CODES.P})
public class ProviderManagerTest {
private AssetManager assetManager;
@@ -40,6 +50,7 @@ public class ProviderManagerTest {
@Before
public void setup() throws Exception {
assetManager = mock(AssetManager.class);
+ BitmaskCoreProvider.initBitmaskMobile(TestSetupHelper.getCustomBitmaskCore());
when(assetManager.open(anyString())).thenAnswer(new Answer<InputStream>() {
@Override
@@ -67,7 +78,7 @@ public class ProviderManagerTest {
preferenceHelper = new PreferenceHelper(mockSharedPrefs);
HashSet<Provider> customProviders = new HashSet<>();
- customProviders.add(Provider.createCustomProvider("https://leapcolombia.org", "leapcolombia.org"));
+ customProviders.add(Provider.createCustomProvider("https://leapcolombia.org", "leapcolombia.org", null));
PreferenceHelper.setCustomProviders(customProviders);
}
@@ -162,7 +173,7 @@ public class ProviderManagerTest {
providerManager.setAddDummyEntry(true);
providerManager.clear();
assertEquals("1 providers", 1, providerManager.providers().size());
- assertEquals("provider is dummy element", "https://example.net", providerManager.get(0).getMainUrlString());
+ assertEquals("provider is dummy element", "", providerManager.get(0).getMainUrl());
}
@Test
@@ -195,8 +206,8 @@ public class ProviderManagerTest {
providerManager.add(secondCustomProvider);
providerManager.saveCustomProviders();
Set<String> providerSet = mockSharedPrefs.getStringSet(Constants.CUSTOM_PROVIDER_DOMAINS, new HashSet<>());
- assertEquals("persist was called twice", 2, providerSet.size());
- assertEquals("PreferenceHelper has 2 providers", 2, PreferenceHelper.getCustomProviders().size());
+ assertEquals("persist was called twice", 3, providerSet.size());
+ assertEquals("PreferenceHelper has 2 providers", 3, PreferenceHelper.getCustomProviders().size());
}
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java b/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java
index 1b94042e..cb226033 100644
--- a/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java
@@ -10,7 +10,6 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
-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_MOTD;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_HASHES;
@@ -21,7 +20,6 @@ import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICA
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getEipDefinitionFromPreferences;
import android.content.Context;
-import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@@ -30,9 +28,6 @@ import android.os.Bundle;
import android.os.Parcelable;
import android.os.ResultReceiver;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
import org.json.JSONException;
import org.json.JSONObject;
import org.mockito.Mockito;
@@ -47,17 +42,9 @@ import java.math.BigInteger;
import java.net.UnknownHostException;
import java.security.interfaces.RSAPrivateKey;
import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Base64;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
import java.util.Set;
-import java.util.Vector;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicBoolean;
import okhttp3.OkHttpClient;
import se.leap.bitmaskclient.R;
@@ -65,16 +52,14 @@ import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.base.models.ProviderObservable;
import se.leap.bitmaskclient.base.utils.CertificateHelper;
import se.leap.bitmaskclient.base.utils.FileHelper;
-import se.leap.bitmaskclient.base.utils.InputStreamHelper;
import se.leap.bitmaskclient.base.utils.BuildConfigHelper;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
-import se.leap.bitmaskclient.base.utils.RSAHelper;
+import se.leap.bitmaskclient.base.utils.PrivateKeyHelper;
import se.leap.bitmaskclient.providersetup.ProviderApiConnector;
import se.leap.bitmaskclient.providersetup.connectivity.DnsResolver;
import se.leap.bitmaskclient.providersetup.connectivity.OkHttpClientGenerator;
import se.leap.bitmaskclient.testutils.BackendMockResponses.BackendMockProvider;
import se.leap.bitmaskclient.testutils.matchers.BundleMatcher;
-import se.leap.bitmaskclient.tor.TorStatusObservable;
/**
* Created by cyberta on 29.01.18.
@@ -160,8 +145,8 @@ public class MockHelper {
return new FileHelper(new MockFileHelper(mockedFile));
}
- public static RSAHelper mockRSAHelper() {
- return new RSAHelper(rsaKeyString -> new RSAPrivateKey() {
+ public static PrivateKeyHelper mockPrivateKeyHelper() {
+ return new PrivateKeyHelper(rsaKeyString -> new RSAPrivateKey() {
@Override
public BigInteger getPrivateExponent() {
return BigInteger.TEN;
@@ -218,8 +203,8 @@ public class MockHelper {
}
@Override
- public boolean useKcp() {
- return false;
+ public String obfsvpnTransportProtocol() {
+ return "tcp";
}
@Override
@@ -237,7 +222,7 @@ public class MockHelper {
PreferenceHelper preferenceHelper = new PreferenceHelper(sharedPreferences);
sharedPreferences.edit().
- putString(PROVIDER_PRIVATE_KEY, providerFromPrefs.getPrivateKey()).
+ putString(PROVIDER_PRIVATE_KEY, providerFromPrefs.getPrivateKeyString()).
putString(PROVIDER_VPN_CERTIFICATE, providerFromPrefs.getVpnCertificate()).
putString(Provider.KEY, providerFromPrefs.getDefinitionString()).
putString(Provider.CA_CERT_FINGERPRINT, providerFromPrefs.getCaCertFingerprint()).
@@ -257,7 +242,7 @@ public class MockHelper {
PreferenceHelper preferenceHelper = new PreferenceHelper(sharedPreferences);
sharedPreferences.edit().
- putString(PROVIDER_PRIVATE_KEY, provider.getPrivateKey()).
+ putString(PROVIDER_PRIVATE_KEY, provider.getPrivateKeyString()).
putString(PROVIDER_VPN_CERTIFICATE, provider.getVpnCertificate()).
putString(Provider.KEY, provider.getDefinitionString()).
putString(Provider.CA_CERT_FINGERPRINT, provider.getCaCertFingerprint()).
@@ -274,9 +259,9 @@ public class MockHelper {
sharedPreferences.edit().
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()).
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java b/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java
index 72791ba6..752a5336 100644
--- a/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java
@@ -25,6 +25,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import mobilemodels.BitmaskMobileCore;
import se.leap.bitmaskclient.base.models.Provider;
/**
@@ -105,4 +106,83 @@ public class TestSetupHelper {
return null;
}
+ public static BitmaskMobileCore getCustomBitmaskCore() {
+ return new BitmaskMobileCore() {
+ @Override
+ public String getAllBridges(String s, String s1, String s2, String s3) throws Exception {
+ return null;
+ }
+
+ @Override
+ public String getAllGateways(String s, String s1, String s2) throws Exception {
+ return null;
+ }
+
+ @Override
+ public String getGeolocation() throws Exception {
+ return null;
+ }
+
+ @Override
+ public String getIntroducerURLByDomain(String s) throws Exception {
+ return null;
+ }
+
+ @Override
+ public String getOpenVPNCert() throws Exception {
+ return null;
+ }
+
+ @Override
+ public String getProvider() throws Exception {
+ return null;
+ }
+
+ @Override
+ public String getService() throws Exception {
+ return null;
+ }
+
+ @Override
+ public void setCountryCode(String s) {
+
+ }
+
+ @Override
+ public void setCountryCodeLookupURL(String s) throws Exception {
+
+ }
+
+ @Override
+ public void setDebug(boolean b) {
+
+ }
+
+ @Override
+ public void setIntroducer(String s) throws Exception {
+
+ }
+
+ @Override
+ public void setResolveWithDoH(boolean b) {
+
+ }
+
+ @Override
+ public void setSocksProxy(String s) {
+
+ }
+
+ @Override
+ public void setStunServers(String s) throws Exception {
+
+ }
+
+ @Override
+ public void setUseTls(boolean b) {
+
+ }
+ };
+ }
+
}
diff --git a/app/src/test/resources/ed25519_credentials.pem b/app/src/test/resources/ed25519_credentials.pem
new file mode 100644
index 00000000..1c10ed28
--- /dev/null
+++ b/app/src/test/resources/ed25519_credentials.pem
@@ -0,0 +1,31 @@
+<key>
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIF+HZvpSdhnTbYeT635bT2+IU4FbW3EWlHuUnXvhb10m
+-----END PRIVATE KEY-----
+</key>
+<cert>
+-----BEGIN CERTIFICATE-----
+MIIBgzCCASigAwIBAgIRALD3Z4SsobpcU7tcC0r9JOQwCgYIKoZIzj0EAwIwNzE1
+MDMGA1UEAwwsUHJvdmlkZXIgUm9vdCBDQSAoY2xpZW50IGNlcnRpZmljYXRlcyBv
+bmx5ISkwHhcNMjQxMTA1MTU0MjU0WhcNMjQxMTI5MTU0MjU0WjAUMRIwEAYDVQQD
+EwlVTkxJTUlURUQwKjAFBgMrZXADIQC5QkZAcpkQ3Rm54gN5iLEU1Zp1w+patXVT
+W9GRXmFz+6NnMGUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMC
+MB0GA1UdDgQWBBRMxeMW4vqGK7FBkDt2+8upfkK1kzAfBgNVHSMEGDAWgBS0pVQs
+1wnvNYG0AnmkxUcLOw+BLDAKBggqhkjOPQQDAgNJADBGAiEAg112+zWMm9qrPTvK
+99IMa+wbeNzZLSoN9xewf5rxOX0CIQCvMi08JcajsAJ9Dg6YAQgpmFdb35HDCzve
+lhkTCWJpgQ==
+-----END CERTIFICATE-----
+</cert>
+<ca>
+-----BEGIN CERTIFICATE-----
+MIIBozCCAUigAwIBAgIBATAKBggqhkjOPQQDAjA3MTUwMwYDVQQDDCxQcm92aWRl
+ciBSb290IENBIChjbGllbnQgY2VydGlmaWNhdGVzIG9ubHkhKTAeFw0yNDEwMjMx
+MjA0MjRaFw0yOTEwMjMxMjA5MjRaMDcxNTAzBgNVBAMMLFByb3ZpZGVyIFJvb3Qg
+Q0EgKGNsaWVudCBjZXJ0aWZpY2F0ZXMgb25seSEpMFkwEwYHKoZIzj0CAQYIKoZI
+zj0DAQcDQgAEMImwbNTDrXMeWfyTb2TMNzXNr79OsKjLDdZWqVT0iHMI8apo2P4H
+eXCHVGjS2Z+jpyI1u9ic3igThsKEmdZMSKNFMEMwDgYDVR0PAQH/BAQDAgKkMBIG
+A1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFLSlVCzXCe81gbQCeaTFRws7D4Es
+MAoGCCqGSM49BAMCA0kAMEYCIQCw88nXg/vs/KgGqH1uPs9oZkOxucVn/ZEznYzg
+szLhtAIhAPY32oHwmj3yHO9H2Jp7x0CoHuu1fKd9fQTBvEEbi7o9
+-----END CERTIFICATE-----
+</ca>
diff --git a/app/src/test/resources/multiple_pts_per_host_eip-service.json b/app/src/test/resources/multiple_pts_per_host_eip-service.json
new file mode 100644
index 00000000..c2fec356
--- /dev/null
+++ b/app/src/test/resources/multiple_pts_per_host_eip-service.json
@@ -0,0 +1,145 @@
+{
+ "gateways":[
+ {
+ "capabilities":{
+ "adblock":false,
+ "filter_dns":false,
+ "limited":false,
+ "transport":[
+ {
+ "ports":[
+ "80"
+ ],
+ "protocols":[
+ "tcp",
+ "udp"
+ ],
+ "type":"openvpn"
+ },
+ {
+ "options":{
+ "cert":"XXXX",
+ "iatMode":"0"
+ },
+ "ports":[
+ "443"
+ ],
+ "protocols":[
+ "tcp"
+ ],
+ "type":"obfs4"
+ },
+ {
+ "options":{
+ "cert":"XXXX",
+ "iatMode":"0"
+ },
+ "ports":[
+ "4431"
+ ],
+ "protocols":[
+ "kcp"
+ ],
+ "type":"obfs4"
+ },
+ {
+ "options":{
+ "cert":"XXXX",
+ "iatMode":"0"
+ },
+ "ports":[
+ "4432"
+ ],
+ "protocols":[
+ "quic"
+ ],
+ "type":"obfs4"
+ }
+ ]
+ },
+ "host":"cod.demo.bitmask.net",
+ "ip_address":"37.218.245.94",
+ "location":"North Brabant"
+ },
+ {
+ "capabilities":{
+ "adblock":false,
+ "filter_dns":false,
+ "limited":false,
+ "transport":[
+ {
+ "ports":[
+ "80"
+ ],
+ "protocols":[
+ "tcp",
+ "udp"
+ ],
+ "type":"openvpn"
+ },
+ {
+ "options":{
+ "cert":"XXXX",
+ "iatMode":"0"
+ },
+ "ports":[
+ "443"
+ ],
+ "protocols":[
+ "udp"
+ ],
+ "type":"obfs4"
+ },
+ {
+ "options":{
+ "cert":"XXXX",
+ "iatMode":"0"
+ },
+ "ports":[
+ "4431"
+ ],
+ "protocols":[
+ "kcp"
+ ],
+ "type":"obfs4"
+ }
+ ]
+ },
+ "host":"mullet.demo.bitmask.net",
+ "ip_address":"37.218.241.208",
+ "location":"Florida"
+ }
+ ],
+ "locations":{
+ "Florida":{
+ "country_code":"US",
+ "hemisphere":"N",
+ "name":"United States",
+ "timezone":"-6"
+ },
+ "North Brabant":{
+ "country_code":"NL",
+ "hemisphere":"N",
+ "name":"Netherlands",
+ "timezone":"+2"
+ }
+ },
+ "openvpn_configuration":{
+ "auth":"SHA512",
+ "cipher":"AES-256-GCM",
+ "data-ciphers":"AES-256-GCM",
+ "dev":"tun",
+ "float":"",
+ "keepalive":"10 30",
+ "key-direction":"1",
+ "nobind":true,
+ "persist-key":true,
+ "rcvbuf":"0",
+ "sndbuf":"0",
+ "tls-cipher":"TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384",
+ "tls-version-min":"1.2",
+ "verb":"3"
+ },
+ "serial":3,
+ "version":3
+} \ No newline at end of file
diff --git a/app/src/test/resources/private_PKCS8_encoded_ecdsa_key.pem b/app/src/test/resources/private_PKCS8_encoded_ecdsa_key.pem
new file mode 100644
index 00000000..568783a1
--- /dev/null
+++ b/app/src/test/resources/private_PKCS8_encoded_ecdsa_key.pem
@@ -0,0 +1,8 @@
+-----BEGIN PRIVATE KEY-----
+MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAQHgofCij0Tdc8aO5
+lNnxhjXiU2Z+84/vz0RpCdoZt0H8ytLb5AOUOaPMu5gNGC2SssTkJhGc/dDX7jdw
+8/GEQQ2hgYkDgYYABABnVEIseHS5WQ/8J3x//uTaU9G5d3HR/dOo+RI7PLizxj8p
+pLKptfPDLTWHMujqE5yPe4GYnU2S1KMws853VBTucwF4AVz1sxYMEpFcYUys+Xr1
+JyTDsxA/o4yeV4ZcuqaofNFYUL6YRFjXg7UAlUPp0s6ongQzJ0/10wGDbUI7vR0I
+Lg==
+-----END PRIVATE KEY----- \ No newline at end of file
diff --git a/app/src/test/resources/private_PKCS8_encoded_ed25519_key.pem b/app/src/test/resources/private_PKCS8_encoded_ed25519_key.pem
new file mode 100644
index 00000000..eac4d4db
--- /dev/null
+++ b/app/src/test/resources/private_PKCS8_encoded_ed25519_key.pem
@@ -0,0 +1,3 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIGVuE1J3PTf1TRv1xz5bXCyh5oXa3MieBg+Re9qGNZvU
+-----END PRIVATE KEY----- \ No newline at end of file