summaryrefslogtreecommitdiff
path: root/app/src
diff options
context:
space:
mode:
authorcyberta <cyberta@riseup.net>2021-11-12 00:46:35 +0000
committercyberta <cyberta@riseup.net>2021-11-12 00:46:35 +0000
commitc5d722f555b952407dade3abb1ffd537e6747317 (patch)
treea9ebb8b33438589a33ed9ce54ade50371c9fe147 /app/src
parent571c0479f7400e56cfdb27408160d8a816cc8610 (diff)
parent8aeb4791b6e024de9aa9c61b574d8c798a3c0a2c (diff)
Merge branch 'tor-snowflake' into 'master'
tor-over-snowflake Closes #9045 See merge request leap/bitmask_android!138
Diffstat (limited to 'app/src')
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/NetworkUtils.java6
-rw-r--r--app/src/main/java/de/blinkt/openvpn/core/OpenVPNThread.java6
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java4
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java4
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java11
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java18
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/DefaultedURL.java2
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java4
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java32
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/EIP.java6
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java57
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java5
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/VpnNotificationManager.java8
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Shapeshifter.java4
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java55
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPICommand.java18
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiConnector.java13
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java122
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiSetupBroadcastReceiver.java4
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java27
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java59
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupInterface.java2
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java216
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/LoginActivity.java2
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderSetupBaseActivity.java32
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SignupActivity.java2
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java25
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java178
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/TorNotificationManager.java128
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/TorServiceCommand.java138
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/TorServiceConnection.java88
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java290
-rw-r--r--app/src/main/res/drawable/ic_snowflake.pngbin0 -> 2653 bytes
-rw-r--r--app/src/main/res/drawable/ic_tor.pngbin0 -> 10221 bytes
-rw-r--r--app/src/main/res/drawable/v_vertical_gradient.xml8
-rw-r--r--app/src/main/res/layout-xlarge/v_loading_screen.xml176
-rw-r--r--app/src/main/res/layout/v_add_provider.xml41
-rw-r--r--app/src/main/res/layout/v_loading_screen.xml175
-rw-r--r--app/src/main/res/layout/v_log_item.xml29
-rw-r--r--app/src/main/res/values/strings.xml27
-rw-r--r--app/src/production/java/se/leap/bitmaskclient/providersetup/ProviderApiManager.java60
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java66
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java237
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BackendMockProvider.java9
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/GeoIpServiceNotReachableTorFallbackBackendResponse.java89
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/TorFallbackBackendResponse.java86
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java91
-rw-r--r--app/src/test/resources/error_messages.json4
-rw-r--r--app/src/test/resources/v4/riseup.net.json2
51 files changed, 2435 insertions, 237 deletions
diff --git a/app/src/main/java/de/blinkt/openvpn/core/NetworkUtils.java b/app/src/main/java/de/blinkt/openvpn/core/NetworkUtils.java
index 78799a3a..a4f09806 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/NetworkUtils.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/NetworkUtils.java
@@ -9,6 +9,9 @@ import android.content.Context;
import android.net.*;
import android.os.Build;
import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.core.net.ConnectivityManagerCompat;
import java.net.Inet4Address;
import java.net.Inet6Address;
@@ -16,6 +19,8 @@ import java.util.Vector;
public class NetworkUtils {
+ private static final String TAG = NetworkUtils.class.getSimpleName();
+
public static Vector<String> getLocalNetworks(Context c, boolean ipv6) {
Vector<String> nets = new Vector<>();
ConnectivityManager conn = (ConnectivityManager) c.getSystemService(Context.CONNECTIVITY_SERVICE);
@@ -75,5 +80,4 @@ public class NetworkUtils {
}
return nets;
}
-
} \ No newline at end of file
diff --git a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNThread.java b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNThread.java
index fc77d9a5..9d307b02 100644
--- a/app/src/main/java/de/blinkt/openvpn/core/OpenVPNThread.java
+++ b/app/src/main/java/de/blinkt/openvpn/core/OpenVPNThread.java
@@ -140,6 +140,8 @@ public class OpenVPNThread implements Runnable {
InputStream in = mProcess.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
+ // 1380308330.240114 18000002 Send to HTTP proxy: 'X-Online-Host: bla.blabla.com'
+ Pattern p = Pattern.compile("(\\d+).(\\d+) ([0-9a-f])+ (.*)");
while (true) {
String logline = br.readLine();
if (logline == null)
@@ -151,10 +153,6 @@ public class OpenVPNThread implements Runnable {
if (logline.startsWith(BROKEN_PIE_SUPPORT) || logline.contains(BROKEN_PIE_SUPPORT2))
mBrokenPie = true;
-
- // 1380308330.240114 18000002 Send to HTTP proxy: 'X-Online-Host: bla.blabla.com'
-
- Pattern p = Pattern.compile("(\\d+).(\\d+) ([0-9a-f])+ (.*)");
Matcher m = p.matcher(logline);
int logerror = 0;
if (m.matches()) {
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 4b6fea72..60c28a9a 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java
@@ -34,6 +34,8 @@ import se.leap.bitmaskclient.eip.EipSetupObserver;
import se.leap.bitmaskclient.base.models.ProviderObservable;
import se.leap.bitmaskclient.tethering.TetheringStateManager;
import se.leap.bitmaskclient.base.utils.PRNGFixes;
+import se.leap.bitmaskclient.tor.TorNotificationManager;
+import se.leap.bitmaskclient.tor.TorStatusObservable;
import static android.content.Intent.CATEGORY_DEFAULT;
import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_DOWNLOAD_SERVICE_EVENT;
@@ -53,6 +55,7 @@ public class BitmaskApp extends MultiDexApplication {
private RefWatcher refWatcher;
private ProviderObservable providerObservable;
private DownloadBroadcastReceiver downloadBroadcastReceiver;
+ private TorStatusObservable torStatusObservable;
@Override
@@ -69,6 +72,7 @@ public class BitmaskApp extends MultiDexApplication {
SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
providerObservable = ProviderObservable.getInstance();
providerObservable.updateProvider(getSavedProviderFromSharedPreferences(preferences));
+ torStatusObservable = TorStatusObservable.getInstance();
EipSetupObserver.init(this, preferences);
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
TetheringStateManager.getInstance().init(this);
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java b/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java
index 126c4a98..18ac8b7c 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/MainActivity.java
@@ -195,13 +195,14 @@ public class MainActivity extends AppCompatActivity implements EipSetupListener,
storeProviderInPreferences(preferences, provider);
ProviderObservable.getInstance().updateProvider(provider);
if (!provider.supportsPluggableTransports()) {
- PreferenceHelper.usePluggableTransports(this, false);
+ PreferenceHelper.useBridges(this, false);
}
navigationDrawerFragment.refresh();
switch (requestCode) {
case REQUEST_CODE_SWITCH_PROVIDER:
EipCommand.stopVPN(this.getApplicationContext());
+ EipCommand.startVPN(this.getApplicationContext(), false);
break;
case REQUEST_CODE_CONFIGURE_LEAP:
Log.d(TAG, "REQUEST_CODE_CONFIGURE_LEAP - onActivityResult - MainActivity");
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 84024962..d1222cd7 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
@@ -56,7 +56,7 @@ import static android.view.View.VISIBLE;
import static se.leap.bitmaskclient.base.MainActivity.ACTION_SHOW_VPN_FRAGMENT;
import static se.leap.bitmaskclient.base.models.Constants.PREFERRED_CITY;
import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
-import static se.leap.bitmaskclient.base.models.Constants.USE_PLUGGABLE_TRANSPORTS;
+import static se.leap.bitmaskclient.base.models.Constants.USE_BRIDGES;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getPreferredCity;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.setPreferredCity;
@@ -162,7 +162,7 @@ public class GatewaySelectionFragment extends Fragment implements SharedPreferen
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
- if (USE_PLUGGABLE_TRANSPORTS.equals(key)) {
+ if (USE_BRIDGES.equals(key)) {
locationListAdapter.updateData(gatewaysManager.getGatewayLocations());
setVpnButtonState();
} else if (PREFERRED_CITY.equals(key)) {
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 4034bd04..86f8471c 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
@@ -18,7 +18,6 @@ package se.leap.bitmaskclient.base.fragments;
import android.app.Dialog;
import android.content.Context;
-import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -42,8 +41,8 @@ import static se.leap.bitmaskclient.eip.EIP.EIPErrors.UNKNOWN;
import static se.leap.bitmaskclient.eip.EIP.EIPErrors.valueOf;
import static se.leap.bitmaskclient.eip.EIP.ERRORS;
import static se.leap.bitmaskclient.eip.EIP.ERRORID;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUsePluggableTransports;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.usePluggableTransports;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseBridges;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useBridges;
/**
* Implements an error dialog for the main activity.
@@ -129,14 +128,14 @@ public class MainActivityErrorDialog extends DialogFragment {
EipCommand.startVPN(applicationContext, false);
});
} else if (provider.supportsPluggableTransports()) {
- if (getUsePluggableTransports(applicationContext)) {
+ if (getUseBridges(applicationContext)) {
builder.setPositiveButton(warning_option_try_ovpn, ((dialog, which) -> {
- usePluggableTransports(applicationContext, false);
+ useBridges(applicationContext, false);
EipCommand.startVPN(applicationContext, false);
}));
} else {
builder.setPositiveButton(warning_option_try_pt, ((dialog, which) -> {
- usePluggableTransports(applicationContext, true);
+ useBridges(applicationContext, true);
EipCommand.startVPN(applicationContext, false);
}));
}
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 5cae1591..a13692a5 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
@@ -76,7 +76,7 @@ import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_SWITCH_PROVIDER;
import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
import static se.leap.bitmaskclient.base.models.Constants.USE_IPv6_FIREWALL;
-import static se.leap.bitmaskclient.base.models.Constants.USE_PLUGGABLE_TRANSPORTS;
+import static se.leap.bitmaskclient.base.models.Constants.USE_BRIDGES;
import static se.leap.bitmaskclient.R.string.about_fragment_title;
import static se.leap.bitmaskclient.R.string.exclude_apps_fragment_title;
import static se.leap.bitmaskclient.R.string.log_fragment_title;
@@ -84,10 +84,10 @@ import static se.leap.bitmaskclient.base.utils.ConfigHelper.isDefaultBitmask;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getPreferredCity;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getSaveBattery;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getShowAlwaysOnDialog;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUsePluggableTransports;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseBridges;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.saveBattery;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.showExperimentalFeatures;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.usePluggableTransports;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useBridges;
/**
* Fragment used for managing interactions for and presentation of a navigation drawer.
@@ -289,8 +289,10 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen
if (isDefaultBitmask()) {
IconTextEntry switchProvider = drawerView.findViewById(R.id.switch_provider);
switchProvider.setVisibility(VISIBLE);
- switchProvider.setOnClickListener(v ->
- getActivity().startActivityForResult(new Intent(getActivity(), ProviderListActivity.class), REQUEST_CODE_SWITCH_PROVIDER));
+ switchProvider.setOnClickListener(v -> {
+ closeDrawer();
+ getActivity().startActivityForResult(new Intent(getActivity(), ProviderListActivity.class), REQUEST_CODE_SWITCH_PROVIDER);
+ });
}
}
@@ -298,12 +300,12 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen
IconSwitchEntry useBridges = drawerView.findViewById(R.id.bridges_switch);
if (ProviderObservable.getInstance().getCurrentProvider().supportsPluggableTransports()) {
useBridges.setVisibility(VISIBLE);
- useBridges.setChecked(getUsePluggableTransports(getContext()));
+ useBridges.setChecked(getUseBridges(getContext()));
useBridges.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (!buttonView.isPressed()) {
return;
}
- usePluggableTransports(getContext(), isChecked);
+ useBridges(getContext(), isChecked);
if (VpnStatus.isVPNActive()) {
EipCommand.startVPN(getContext(), false);
closeDrawer();
@@ -673,7 +675,7 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
- if (key.equals(USE_PLUGGABLE_TRANSPORTS)) {
+ if (key.equals(USE_BRIDGES)) {
initUseBridgesEntry();
} else if (key.equals(USE_IPv6_FIREWALL)) {
initFirewallEntry();
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 3edfbb3d..d91880c6 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
@@ -32,7 +32,7 @@ public interface Constants {
String CLEARLOG = "clearlogconnect";
String LAST_USED_PROFILE = "last_used_profile";
String EXCLUDED_APPS = "excluded_apps";
- String USE_PLUGGABLE_TRANSPORTS = "usePluggableTransports";
+ String USE_BRIDGES = "usePluggableTransports";
String ALLOW_TETHERING_BLUETOOTH = "tethering_bluetooth";
String ALLOW_TETHERING_WIFI = "tethering_wifi";
String ALLOW_TETHERING_USB = "tethering_usb";
@@ -41,6 +41,7 @@ public interface Constants {
String RESTART_ON_UPDATE = "restart_on_update";
String LAST_UPDATE_CHECK = "last_update_check";
String PREFERRED_CITY = "preferred_city";
+ String USE_TOR = "use_tor";
//////////////////////////////////////////////
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/DefaultedURL.java b/app/src/main/java/se/leap/bitmaskclient/base/models/DefaultedURL.java
index 4bb7e4ee..81f0d0c7 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/models/DefaultedURL.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/DefaultedURL.java
@@ -5,12 +5,12 @@ import java.net.URL;
public class DefaultedURL {
private URL DEFAULT_URL;
- private String default_url = "https://example.net";
private URL url;
DefaultedURL() {
try {
+ String default_url = "https://example.net";
DEFAULT_URL = new URL(default_url);
url = DEFAULT_URL;
} catch (MalformedURLException e) {
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 5d8b4e5d..974aef80 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
@@ -53,8 +53,8 @@ public final class Provider implements Parcelable {
private DefaultedURL mainUrl = new DefaultedURL();
private DefaultedURL apiUrl = new DefaultedURL();
private DefaultedURL geoipUrl = new DefaultedURL();
- private String providerIp = "";
- private String providerApiIp = "";
+ 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 = "";
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 a3d1314e..06fb25e9 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
@@ -14,6 +14,7 @@ import java.util.Set;
import de.blinkt.openvpn.VpnProfile;
import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.tor.TorStatusObservable;
import static android.content.Context.MODE_PRIVATE;
import static se.leap.bitmaskclient.base.models.Constants.ALLOW_TETHERING_BLUETOOTH;
@@ -33,7 +34,8 @@ import static se.leap.bitmaskclient.base.models.Constants.RESTART_ON_UPDATE;
import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
import static se.leap.bitmaskclient.base.models.Constants.SHOW_EXPERIMENTAL;
import static se.leap.bitmaskclient.base.models.Constants.USE_IPv6_FIREWALL;
-import static se.leap.bitmaskclient.base.models.Constants.USE_PLUGGABLE_TRANSPORTS;
+import static se.leap.bitmaskclient.base.models.Constants.USE_BRIDGES;
+import static se.leap.bitmaskclient.base.models.Constants.USE_TOR;
/**
* Created by cyberta on 18.03.18.
@@ -140,12 +142,31 @@ public class PreferenceHelper {
return getBoolean(context, RESTART_ON_UPDATE, false);
}
- public static boolean getUsePluggableTransports(Context context) {
- return getBoolean(context, USE_PLUGGABLE_TRANSPORTS, false);
+
+ public static boolean getUseBridges(SharedPreferences preferences) {
+ return preferences.getBoolean(USE_BRIDGES, false);
}
- public static void usePluggableTransports(Context context, boolean isEnabled) {
- putBoolean(context, USE_PLUGGABLE_TRANSPORTS, isEnabled);
+ public static boolean getUseBridges(Context context) {
+ return getBoolean(context, USE_BRIDGES, false);
+ }
+
+ public static void useBridges(Context context, boolean isEnabled) {
+ putBoolean(context, USE_BRIDGES, isEnabled);
+ putBoolean(context, USE_TOR, isEnabled);
+ if (!isEnabled) {
+ TorStatusObservable.setProxyPort(-1);
+ }
+ }
+
+ // in contrast to USE_BRIDGES, USE_TOR in enabled by default
+ // This way the initial provider setup can rely on tor as fallback circumvention mechanism
+ public static Boolean getUseTor(SharedPreferences preferences) {
+ return preferences.getBoolean(USE_TOR, true);
+ }
+
+ public static Boolean getUseTor(Context context) {
+ return getBoolean(context, USE_TOR, true);
}
public static void saveBattery(Context context, boolean isEnabled) {
@@ -278,5 +299,4 @@ public class PreferenceHelper {
SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
preferences.edit().putBoolean(key, value).apply();
}
-
}
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 6919a532..dd9054f1 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java
@@ -85,7 +85,7 @@ import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICA
import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
import static se.leap.bitmaskclient.base.utils.ConfigHelper.ensureNotOnMainThread;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getPreferredCity;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUsePluggableTransports;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseBridges;
import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_INVALID_PROFILE;
import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_INVALID_VPN_CERTIFICATE;
import static se.leap.bitmaskclient.eip.EIP.EIPErrors.ERROR_VPN_PREPARE;
@@ -315,7 +315,7 @@ public final class EIP extends JobIntentService implements Observer {
*/
private void launchActiveGateway(Gateway gateway, int nClosestGateway, Bundle result) {
VpnProfile profile;
- Connection.TransportType transportType = getUsePluggableTransports(this) ? OBFS4 : OPENVPN;
+ Connection.TransportType transportType = getUseBridges(this) ? OBFS4 : OPENVPN;
if (gateway == null ||
(profile = gateway.getProfile(transportType)) == null) {
String preferredLocation = getPreferredCity(getApplicationContext());
@@ -537,7 +537,7 @@ public final class EIP extends JobIntentService implements Observer {
if (isManualGatewaySelection) {
return R.string.warning_no_more_gateways_manual_gw_selection;
} else if (ProviderObservable.getInstance().getCurrentProvider().supportsPluggableTransports()) {
- if (PreferenceHelper.getUsePluggableTransports(getApplicationContext())) {
+ if (PreferenceHelper.getUseBridges(getApplicationContext())) {
return R.string.warning_no_more_gateways_use_ovpn;
} else {
return R.string.warning_no_more_gateways_use_pt;
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 1ad5f7d2..023a1ce1 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java
@@ -28,27 +28,31 @@ import android.util.Log;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.json.JSONObject;
+import org.torproject.jni.TorService;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
-import de.blinkt.openvpn.LaunchVPN;
import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.ConnectionStatus;
import de.blinkt.openvpn.core.LogItem;
import de.blinkt.openvpn.core.VpnStatus;
+import se.leap.bitmaskclient.appUpdate.DownloadServiceCommand;
import se.leap.bitmaskclient.base.models.Provider;
-import se.leap.bitmaskclient.providersetup.ProviderAPI;
-import se.leap.bitmaskclient.providersetup.ProviderAPICommand;
import se.leap.bitmaskclient.base.models.ProviderObservable;
-import se.leap.bitmaskclient.appUpdate.DownloadServiceCommand;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import se.leap.bitmaskclient.providersetup.ProviderAPI;
+import se.leap.bitmaskclient.providersetup.ProviderAPICommand;
+import se.leap.bitmaskclient.tor.TorServiceCommand;
+import se.leap.bitmaskclient.tor.TorServiceConnection;
+import se.leap.bitmaskclient.tor.TorStatusObservable;
import static android.app.Activity.RESULT_CANCELED;
import static android.content.Intent.CATEGORY_DEFAULT;
import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_CONNECTING_NO_SERVER_REPLY_YET;
import static de.blinkt.openvpn.core.ConnectionStatus.LEVEL_NOTCONNECTED;
+import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.CHECK_VERSION_FILE;
import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_EIP_EVENT;
import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT;
import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_PROVIDER_API_EVENT;
@@ -66,8 +70,13 @@ import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PROFILE;
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_UPDATED_INVALID_VPN_CERTIFICATE;
+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.appUpdate.DownloadServiceCommand.CHECK_VERSION_FILE;
+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.tor.TorStatusObservable.TorStatus.OFF;
/**
* Created by cyberta on 05.12.18.
@@ -92,6 +101,8 @@ public class EipSetupObserver extends BroadcastReceiver implements VpnStatus.Sta
IntentFilter updateIntentFilter = new IntentFilter(BROADCAST_GATEWAY_SETUP_OBSERVER_EVENT);
updateIntentFilter.addAction(BROADCAST_EIP_EVENT);
updateIntentFilter.addAction(BROADCAST_PROVIDER_API_EVENT);
+ updateIntentFilter.addAction(TorService.ACTION_STATUS);
+ updateIntentFilter.addAction(TorService.ACTION_ERROR);
updateIntentFilter.addCategory(CATEGORY_DEFAULT);
LocalBroadcastManager.getInstance(context.getApplicationContext()).registerReceiver(this, updateIntentFilter);
instance = this;
@@ -140,11 +151,32 @@ public class EipSetupObserver extends BroadcastReceiver implements VpnStatus.Sta
case BROADCAST_PROVIDER_API_EVENT:
handleProviderApiEvent(intent);
break;
+ case TorService.ACTION_STATUS:
+ handleTorStatusEvent(intent);
+ break;
+ case TorService.ACTION_ERROR:
+ handleTorErrorEvent(intent);
+ break;
default:
break;
}
}
+ private void handleTorErrorEvent(Intent intent) {
+ String error = intent.getStringExtra(Intent.EXTRA_TEXT);
+ Log.d(TAG, "handle Tor error event: " + error);
+ TorStatusObservable.setLastError(error);
+ }
+
+ private void handleTorStatusEvent(Intent intent) {
+ String status = intent.getStringExtra(TorService.EXTRA_STATUS);
+ Log.d(TAG, "handle Tor status event: " + status);
+ Integer bootstrap = intent.getIntExtra(TorService.EXTRA_STATUS_DETAIL_BOOTSTRAP, -1);
+ String logKey = intent.getStringExtra(TorService.EXTRA_STATUS_DETAIL_LOGKEY);
+ TorStatusObservable.updateState(context, status, bootstrap, logKey);
+ }
+
+
private void handleProviderApiEvent(Intent intent) {
int resultCode = intent.getIntExtra(BROADCAST_RESULT_CODE, RESULT_CANCELED);
Bundle resultData = intent.getParcelableExtra(BROADCAST_RESULT_KEY);
@@ -178,6 +210,18 @@ public class EipSetupObserver extends BroadcastReceiver implements VpnStatus.Sta
case INCORRECTLY_DOWNLOADED_GEOIP_JSON:
maybeStartEipService(resultData);
break;
+ case PROVIDER_NOK:
+ case INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE:
+ case INCORRECTLY_DOWNLOADED_EIP_SERVICE:
+ case INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
+ if (TorStatusObservable.getStatus() != OFF) {
+ TorServiceCommand.stopTorServiceAsync(context);
+ }
+ Log.d(TAG, "PROVIDER NOK - FETCH FAILED");
+ break;
+ case PROVIDER_OK:
+ Log.d(TAG, "PROVIDER OK - FETCH SUCCESSFUL");
+ break;
default:
break;
}
@@ -324,6 +368,9 @@ public class EipSetupObserver extends BroadcastReceiver implements VpnStatus.Sta
setupNClosestGateway.set(0);
observedProfileFromVpnStatus = null;
this.changingGateway.set(changingGateway);
+ if (TorStatusObservable.getStatus() != OFF) {
+ TorServiceCommand.stopTorServiceAsync(context);
+ }
}
/**
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 77ecfc5f..05775d13 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java
@@ -32,7 +32,6 @@ import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -53,7 +52,7 @@ 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.models.Constants.SORTED_GATEWAYS;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getPreferredCity;
-import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUsePluggableTransports;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseBridges;
/**
* @author parmegv
@@ -82,7 +81,7 @@ public class GatewaysManager {
}
public Gateway select(int nClosest, String city) {
- Connection.TransportType transportType = getUsePluggableTransports(context) ? OBFS4 : OPENVPN;
+ Connection.TransportType transportType = getUseBridges(context) ? OBFS4 : OPENVPN;
if (presortedList.size() > 0) {
return getGatewayFromPresortedList(nClosest, transportType, city);
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/VpnNotificationManager.java b/app/src/main/java/se/leap/bitmaskclient/eip/VpnNotificationManager.java
index 6fac0f72..667b8892 100644
--- a/app/src/main/java/se/leap/bitmaskclient/eip/VpnNotificationManager.java
+++ b/app/src/main/java/se/leap/bitmaskclient/eip/VpnNotificationManager.java
@@ -131,7 +131,7 @@ public class VpnNotificationManager {
String notificationChannelNewstatusId, VpnServiceCallback vpnServiceCallback) {
String cancelString;
CharSequence bigmessage = null;
- String ghostIcon = new String(Character.toChars(0x1f309));
+ String bridgeIcon = new String(Character.toChars(0x1f309));
switch (status) {
// show cancel if no connection
@@ -143,7 +143,7 @@ public class VpnNotificationManager {
if (isObfuscated && Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
Spannable spannable = new SpannableString(context.getString(R.string.obfuscated_connection_try));
spannable.setSpan(new StyleSpan(Typeface.ITALIC), 0, spannable.length() -1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- bigmessage = TextUtils.concat(spannable, " " + ghostIcon + "\n" + msg);
+ bigmessage = TextUtils.concat(spannable, " " + bridgeIcon + "\n" + msg);
}
break;
@@ -152,14 +152,14 @@ public class VpnNotificationManager {
if (isObfuscated && Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
Spannable spannable = new SpannableString(context.getString(R.string.obfuscated_connection));
spannable.setSpan(new StyleSpan(Typeface.ITALIC), 0, spannable.length() -1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- bigmessage = TextUtils.concat(spannable, " " + ghostIcon + "\n" + msg);
+ bigmessage = TextUtils.concat(spannable, " " + bridgeIcon + "\n" + msg);
}
default:
cancelString = context.getString(R.string.cancel_connection);
}
if (isObfuscated) {
- msg = ghostIcon + " " + msg;
+ msg = bridgeIcon + " " + msg;
}
NotificationCompat.Action.Builder actionBuilder = new NotificationCompat.Action.
diff --git a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Shapeshifter.java b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Shapeshifter.java
index d39f8bf3..970703cc 100644
--- a/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Shapeshifter.java
+++ b/app/src/main/java/se/leap/bitmaskclient/pluggableTransports/Shapeshifter.java
@@ -37,11 +37,11 @@ public class Shapeshifter implements Observer {
private static final int RETRY_TIME = 4000;
private static final String TAG = Shapeshifter.class.getSimpleName();
- private ShapeShifter shapeShifter;
+ private final shapeshifter.ShapeShifter shapeShifter;
private boolean isErrorHandling;
private boolean noNetwork;
private int retry = 0;
- private Handler reconnectHandler;
+ private final Handler reconnectHandler;
public class ShapeshifterLogger implements shapeshifter.Logger {
@Override
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 23c750a3..709ca651 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java
@@ -20,12 +20,20 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
+import android.net.ConnectivityManager;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.os.Build;
import androidx.annotation.NonNull;
import androidx.core.app.JobIntentService;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+import java.util.concurrent.TimeoutException;
+
import se.leap.bitmaskclient.providersetup.connectivity.OkHttpClientGenerator;
+import se.leap.bitmaskclient.tor.TorServiceCommand;
+import se.leap.bitmaskclient.tor.TorServiceConnection;
import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
@@ -61,6 +69,7 @@ public class ProviderAPI extends JobIntentService implements ProviderApiManagerB
RECEIVER_KEY = "receiver",
ERRORS = "errors",
ERRORID = "errorId",
+ INITIAL_ACTION = "initalAction",
BACKEND_ERROR_KEY = "error",
BACKEND_ERROR_MESSAGE = "message",
USER_MESSAGE = "userMessage",
@@ -82,9 +91,12 @@ public class ProviderAPI extends JobIntentService implements ProviderApiManagerB
CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE = 15,
INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE = 16,
CORRECTLY_DOWNLOADED_GEOIP_JSON = 17,
- INCORRECTLY_DOWNLOADED_GEOIP_JSON = 18;
+ INCORRECTLY_DOWNLOADED_GEOIP_JSON = 18,
+ TOR_TIMEOUT = 19,
+ MISSING_NETWORK_CONNECTION = 20;
ProviderApiManager providerApiManager;
+ private volatile TorServiceConnection torServiceConnection;
//TODO: refactor me, please!
//used in insecure flavor only
@@ -120,9 +132,50 @@ public class ProviderAPI extends JobIntentService implements ProviderApiManagerB
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}
+ @Override
+ public boolean startTorService() throws InterruptedException, IllegalStateException, TimeoutException {
+ return TorServiceCommand.startTorService(this, null);
+ }
+
+ @Override
+ public void stopTorService() {
+ TorServiceCommand.stopTorService(this);
+ }
+
+ @Override
+ public int getTorHttpTunnelPort() {
+ return TorServiceCommand.getHttpTunnelPort(this);
+ }
+
+ @Override
+ public boolean hasNetworkConnection() {
+ try {
+ ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
+ return activeNetwork != null &&
+ activeNetwork.isConnectedOrConnecting();
+ } else {
+ NetworkCapabilities capabilities = cm.getNetworkCapabilities(cm.getActiveNetwork());
+ if (capabilities != null) {
+ return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+ }
+ return false;
+ }
+ } catch (RuntimeException e) {
+ e.printStackTrace();
+ // we don't know, let's try to fetch data anyways then
+ return true;
+ }
+ }
+
+
+
private ProviderApiManager initApiManager() {
SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
OkHttpClientGenerator clientGenerator = new OkHttpClientGenerator(getResources());
return new ProviderApiManager(preferences, getResources(), clientGenerator, this);
}
+
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPICommand.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPICommand.java
index 1408dce8..3cdfcab0 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPICommand.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPICommand.java
@@ -13,12 +13,12 @@ import se.leap.bitmaskclient.base.models.Provider;
public class ProviderAPICommand {
private static final String TAG = ProviderAPICommand.class.getSimpleName();
- private Context context;
+ private final Context context;
- private String action;
- private Bundle parameters;
- private ResultReceiver resultReceiver;
- private Provider provider;
+ private final String action;
+ private final Bundle parameters;
+ private final ResultReceiver resultReceiver;
+ private final Provider provider;
private ProviderAPICommand(@NonNull Context context, @NonNull String action, @NonNull Provider provider, ResultReceiver resultReceiver) {
this(context.getApplicationContext(), action, Bundle.EMPTY, provider, resultReceiver);
@@ -64,22 +64,22 @@ public class ProviderAPICommand {
return command;
}
- public static void execute(Context context, String action, @NonNull Provider provider) {
+ public static void execute(Context context, String action, Provider provider) {
ProviderAPICommand command = new ProviderAPICommand(context, action, provider);
command.execute();
}
- public static void execute(Context context, String action, Bundle parameters, @NonNull Provider provider) {
+ public static void execute(Context context, String action, Bundle parameters, Provider provider) {
ProviderAPICommand command = new ProviderAPICommand(context, action, parameters, provider);
command.execute();
}
- public static void execute(Context context, String action, Bundle parameters, @NonNull Provider provider, ResultReceiver resultReceiver) {
+ public static void execute(Context context, String action, Bundle parameters, Provider provider, ResultReceiver resultReceiver) {
ProviderAPICommand command = new ProviderAPICommand(context, action, parameters, provider, resultReceiver);
command.execute();
}
- public static void execute(Context context, String action, @NonNull Provider provider, ResultReceiver resultReceiver) {
+ public static void execute(Context context, String action, Provider provider, ResultReceiver resultReceiver) {
ProviderAPICommand command = new ProviderAPICommand(context, action, provider, resultReceiver);
command.execute();
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiConnector.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiConnector.java
index c863abd4..35ad9cd2 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiConnector.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiConnector.java
@@ -95,10 +95,15 @@ public class ProviderApiConnector {
if (!response.isSuccessful()) {
VpnStatus.logWarning("[API] API request failed: " + url);
}
- InputStream inputStream = response.body().byteStream();
- Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
- if (scanner.hasNext()) {
- return scanner.next();
+
+ if (response.body() != null) {
+ InputStream inputStream = response.body().byteStream();
+ Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
+ if (scanner.hasNext()) {
+ String result = scanner.next();
+ response.body().close();
+ return result;
+ }
}
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 c5dc6572..52046e07 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java
@@ -48,6 +48,7 @@ import java.security.interfaces.RSAPrivateKey;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
+import java.util.concurrent.TimeoutException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLPeerUnverifiedException;
@@ -59,10 +60,13 @@ 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.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;
import static se.leap.bitmaskclient.R.string.certificate_error;
import static se.leap.bitmaskclient.R.string.error_io_exception_user_message;
@@ -110,9 +114,11 @@ import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLO
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;
@@ -122,12 +128,17 @@ 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_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.tor.TorStatusObservable.TorStatus.OFF;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus.ON;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.getProxyPort;
/**
* Implements the logic of the http api calls. The methods of this class needs to be called from
@@ -140,9 +151,13 @@ public abstract class ProviderApiManagerBase {
public interface ProviderApiServiceCallback {
void broadcastEvent(Intent intent);
+ boolean startTorService() throws InterruptedException, IllegalStateException, TimeoutException;
+ void stopTorService();
+ int getTorHttpTunnelPort();
+ boolean hasNetworkConnection();
}
- private ProviderApiServiceCallback serviceCallback;
+ private final ProviderApiServiceCallback serviceCallback;
protected SharedPreferences preferences;
protected Resources resources;
@@ -156,7 +171,6 @@ public abstract class ProviderApiManagerBase {
}
public void handleIntent(Intent command) {
-// Log.d(TAG, "handleIntent was called!");
ResultReceiver receiver = null;
if (command.getParcelableExtra(RECEIVER_KEY) != null) {
receiver = command.getParcelableExtra(RECEIVER_KEY);
@@ -164,18 +178,42 @@ public abstract class ProviderApiManagerBase {
String action = command.getAction();
Bundle parameters = command.getBundleExtra(PARAMETERS);
- Provider provider = command.getParcelableExtra(PROVIDER_KEY);
+ if (action == null) {
+ Log.e(TAG, "Intent without action sent!");
+ return;
+ }
- if (provider == null) {
+ 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 (action == null) {
- Log.e(TAG, "Intent without action sent!");
+
+ 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.getUseBridges(preferences)) {
+ startTorProxy();
+ }
+ } catch (InterruptedException | IllegalStateException e) {
+ e.printStackTrace();
+ 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:
@@ -269,7 +307,34 @@ public abstract class ProviderApiManagerBase {
}
ProviderObservable.getInstance().setProviderForDns(null);
}
+ break;
+ }
+ }
+
+ protected boolean startTorProxy() throws InterruptedException, IllegalStateException, TimeoutException {
+ if (EipStatus.getInstance().isDisconnected() &&
+ PreferenceHelper.getUseTor(preferences) &&
+ 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, 180);
+ }
+
+ private boolean isTorOnOrCancelled() {
+ return TorStatusObservable.getStatus() == ON || TorStatusObservable.isCancelled();
}
void resetProviderDetails(Provider provider) {
@@ -302,10 +367,11 @@ public abstract class ProviderApiManagerBase {
}
}
- private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage, String errorId) {
+ private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage, String errorId, String initialAction) {
try {
jsonObject.put(ERRORS, errorMessage);
- jsonObject.put(ERRORID, errorId);
+ jsonObject.putOpt(ERRORID, errorId);
+ jsonObject.putOpt(INITIAL_ACTION, initialAction);
} catch (JSONException e) {
e.printStackTrace();
}
@@ -342,7 +408,7 @@ public abstract class ProviderApiManagerBase {
private Bundle register(Provider provider, String username, String password) {
JSONObject stepResult = null;
- OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), stepResult);
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), getProxyPort(), stepResult);
if (okHttpClient == null) {
return backendErrorNotification(stepResult, username);
}
@@ -401,7 +467,7 @@ public abstract class ProviderApiManagerBase {
String providerApiUrl = provider.getApiUrlWithVersion();
- OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), stepResult);
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), getProxyPort(), stepResult);
if (okHttpClient == null) {
return backendErrorNotification(stepResult, username);
}
@@ -678,17 +744,24 @@ public abstract class ProviderApiManagerBase {
}
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(), errorJson);
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), getProxyPort(), errorJson);
if (okHttpClient == null) {
result.putString(ERRORS, errorJson.toString());
return false;
}
- try {
+ if (tries > 0) {
+ result.remove(ERRORS);
+ }
+ try {
return ProviderApiConnector.canConnect(okHttpClient, providerUrl);
} catch (UnknownHostException | SocketTimeoutException e) {
@@ -714,6 +787,19 @@ public abstract class ProviderApiManagerBase {
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;
}
@@ -862,13 +948,13 @@ public abstract class ProviderApiManagerBase {
}
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);
- if (errorId != null) {
- addErrorMessageToJson(errorJson, errorMessage, errorId);
- } else {
- addErrorMessageToJson(errorJson, errorMessage);
- }
+ addErrorMessageToJson(errorJson, errorMessage, errorId, initialAction);
VpnStatus.logWarning("[API] error: " + errorMessage);
result.putString(ERRORS, errorJson.toString());
result.putBoolean(BROADCAST_RESULT_KEY, false);
@@ -950,7 +1036,7 @@ public abstract class ProviderApiManagerBase {
}
private boolean logOut(Provider provider) {
- OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), new JSONObject());
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), getProxyPort(), new JSONObject());
if (okHttpClient == null) {
return false;
}
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 710aee0f..cc6ff149 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiSetupBroadcastReceiver.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiSetupBroadcastReceiver.java
@@ -67,8 +67,10 @@ public class ProviderApiSetupBroadcastReceiver extends BroadcastReceiver {
case ProviderAPI.PROVIDER_OK:
setupInterface.handleProviderSetUp(handledProvider);
break;
+ case ProviderAPI.MISSING_NETWORK_CONNECTION:
+ case ProviderAPI.TOR_TIMEOUT:
case ProviderAPI.PROVIDER_NOK:
- setupInterface.handleProviderSetupFailed(resultData);
+ setupInterface.handleError(resultData);
break;
case ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
setupInterface.handleCorrectlyDownloadedCertificate(handledProvider);
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 654fb8e2..88413087 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java
@@ -1,18 +1,15 @@
package se.leap.bitmaskclient.providersetup;
import android.content.res.AssetManager;
+
import androidx.annotation.VisibleForTesting;
import com.pedrogomez.renderers.AdapteeCollection;
-import org.json.JSONException;
-import org.json.JSONObject;
-
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
-import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
@@ -44,8 +41,8 @@ public class ProviderManager implements AdapteeCollection<Provider> {
private File externalFilesDir;
private Set<Provider> defaultProviders;
private Set<Provider> customProviders;
- private Set<URL> defaultProviderURLs;
- private Set<URL> customProviderURLs;
+ private Set<String> defaultProviderURLs;
+ private Set<String> customProviderURLs;
private static ProviderManager instance;
@@ -76,10 +73,10 @@ public class ProviderManager implements AdapteeCollection<Provider> {
}
}
- private Set<URL> getProviderUrlSetFromProviderSet(Set<Provider> providers) {
- HashSet<URL> providerUrls = new HashSet<>();
+ private Set<String> getProviderUrlSetFromProviderSet(Set<Provider> providers) {
+ HashSet<String> providerUrls = new HashSet<>();
for (Provider provider : providers) {
- providerUrls.add(provider.getMainUrl().getUrl());
+ providerUrls.add(provider.getMainUrl().toString());
}
return providerUrls;
}
@@ -167,16 +164,16 @@ public class ProviderManager implements AdapteeCollection<Provider> {
@Override
public boolean add(Provider element) {
return element != null &&
- !defaultProviderURLs.contains(element.getMainUrl().getUrl()) &&
+ !defaultProviderURLs.contains(element.getMainUrl().toString()) &&
customProviders.add(element) &&
- customProviderURLs.add(element.getMainUrl().getUrl());
+ customProviderURLs.add(element.getMainUrl().toString());
}
@Override
public boolean remove(Object element) {
return element instanceof Provider &&
customProviders.remove(element) &&
- customProviderURLs.remove(((Provider) element).getMainUrl().getUrl());
+ customProviderURLs.remove(((Provider) element).getMainUrl().toString());
}
@Override
@@ -186,7 +183,7 @@ public class ProviderManager implements AdapteeCollection<Provider> {
while (iterator.hasNext()) {
Provider p = (Provider) iterator.next();
addedAll = customProviders.add(p) &&
- customProviderURLs.add(p.getMainUrl().getUrl()) &&
+ customProviderURLs.add(p.getMainUrl().toString()) &&
addedAll;
}
return addedAll;
@@ -199,8 +196,8 @@ public class ProviderManager implements AdapteeCollection<Provider> {
try {
while (iterator.hasNext()) {
Provider p = (Provider) iterator.next();
- removedAll = ((defaultProviders.remove(p) && defaultProviderURLs.remove(p.getMainUrl().getUrl())) ||
- (customProviders.remove(p) && customProviderURLs.remove(p.getMainUrl().getUrl()))) &&
+ removedAll = ((defaultProviders.remove(p) && defaultProviderURLs.remove(p.getMainUrl().toString())) ||
+ (customProviders.remove(p) && customProviderURLs.remove(p.getMainUrl().toString()))) &&
removedAll;
}
} catch (ClassCastException e) {
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 947d1182..a9247807 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java
@@ -21,19 +21,25 @@ import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import org.json.JSONObject;
-import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
-import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.DEFAULT;
-import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.valueOf;
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;
+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.ProviderAPI.UPDATE_PROVIDER_DETAILS;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.DEFAULT;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.valueOf;
/**
* Implements a dialog to show why a download failed.
@@ -43,11 +49,13 @@ import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
public class ProviderSetupFailedDialog extends DialogFragment {
public static String TAG = "downloaded_failed_dialog";
- private final static String KEY_PROVIDER = "key provider";
- private final static String KEY_REASON_TO_FAIL = "key reason to fail";
- private final static String KEY_DOWNLOAD_ERROR = "key download error";
+ private final static String KEY_PROVIDER = "key_provider";
+ private final static String KEY_REASON_TO_FAIL = "key_reason_to_fail";
+ private final static String KEY_DOWNLOAD_ERROR = "key_download_error";
+ private final static String KEY_INITAL_ACTION = "key_inital_action";
private String reasonToFail;
private DOWNLOAD_ERRORS downloadError = DEFAULT;
+ private String initialAction;
private Provider provider;
@@ -59,7 +67,8 @@ public class ProviderSetupFailedDialog extends DialogFragment {
ERROR_CORRUPTED_PROVIDER_JSON,
ERROR_INVALID_CERTIFICATE,
ERROR_CERTIFICATE_PINNING,
- ERROR_NEW_URL_NO_VPN_PROVIDER
+ ERROR_NEW_URL_NO_VPN_PROVIDER,
+ ERROR_TOR_TIMEOUT
}
/**
@@ -91,6 +100,10 @@ public class ProviderSetupFailedDialog extends DialogFragment {
} else if (testNewURL) {
dialogFragment.downloadError = DOWNLOAD_ERRORS.ERROR_NEW_URL_NO_VPN_PROVIDER;
}
+
+ if (errorJson.has(INITIAL_ACTION)) {
+ dialogFragment.initialAction = errorJson.getString(INITIAL_ACTION);
+ }
} catch (Exception e) {
e.printStackTrace();
dialogFragment.reasonToFail = dialogFragment.getString(R.string.error_io_exception_user_message);
@@ -125,6 +138,14 @@ public class ProviderSetupFailedDialog extends DialogFragment {
builder.setPositiveButton(R.string.retry, (dialog, id)
-> interfaceWithConfigurationWizard.addAndSelectNewProvider(provider.getMainUrlString()));
break;
+ case ERROR_TOR_TIMEOUT:
+ builder.setPositiveButton(R.string.retry, (dialog, id) -> {
+ handleTorTimeoutError();
+ });
+ builder.setNeutralButton(R.string.retry_unobfuscated, ((dialog, id) -> {
+ PreferenceHelper.useBridges(getContext(), false);
+ handleTorTimeoutError();
+ }));
default:
builder.setPositiveButton(R.string.retry, (dialog, id)
-> interfaceWithConfigurationWizard.retrySetUpProvider(provider));
@@ -135,6 +156,20 @@ public class ProviderSetupFailedDialog extends DialogFragment {
return builder.create();
}
+ private void handleTorTimeoutError() {
+ switch (initialAction) {
+ case SET_UP_PROVIDER:
+ case UPDATE_PROVIDER_DETAILS:
+ interfaceWithConfigurationWizard.retrySetUpProvider(provider);
+ break;
+ case UPDATE_INVALID_VPN_CERTIFICATE:
+ ProviderAPICommand.execute(getContext(), UPDATE_INVALID_VPN_CERTIFICATE, provider);
+ break;
+ default:
+ break;
+ }
+ }
+
public interface DownloadFailedDialogInterface {
void retrySetUpProvider(@NonNull Provider provider);
@@ -159,6 +194,12 @@ public class ProviderSetupFailedDialog extends DialogFragment {
}
@Override
+ public void onDetach() {
+ super.onDetach();
+ interfaceWithConfigurationWizard = null;
+ }
+
+ @Override
public void onCancel(DialogInterface dialog) {
dialog.dismiss();
interfaceWithConfigurationWizard.cancelSettingUpProvider();
@@ -170,6 +211,7 @@ public class ProviderSetupFailedDialog extends DialogFragment {
outState.putParcelable(KEY_PROVIDER, provider);
outState.putString(KEY_REASON_TO_FAIL, reasonToFail);
outState.putString(KEY_DOWNLOAD_ERROR, downloadError.toString());
+ outState.putString(KEY_INITAL_ACTION, initialAction);
}
private void restoreFromSavedInstance(Bundle savedInstanceState) {
@@ -185,5 +227,8 @@ public class ProviderSetupFailedDialog extends DialogFragment {
if (savedInstanceState.containsKey(KEY_DOWNLOAD_ERROR)) {
this.downloadError = valueOf(savedInstanceState.getString(KEY_DOWNLOAD_ERROR));
}
+ if (savedInstanceState.containsKey(KEY_INITAL_ACTION)) {
+ this.initialAction = savedInstanceState.getString(KEY_INITAL_ACTION);
+ }
}
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupInterface.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupInterface.java
index 5b5c94b4..0c60a3ce 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupInterface.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupInterface.java
@@ -35,7 +35,7 @@ public interface ProviderSetupInterface {
}
void handleProviderSetUp(Provider provider);
- void handleProviderSetupFailed(Bundle resultData);
+ void handleError(Bundle resultData);
void handleCorrectlyDownloadedCertificate(Provider provider);
void handleIncorrectlyDownloadedCertificate();
Provider getProvider();
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java
index b2f13e07..a4104e30 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java
@@ -5,31 +5,49 @@ import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
+import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
+import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.Guideline;
import androidx.core.content.ContextCompat;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.List;
+import java.util.Observable;
+import java.util.Observer;
import butterknife.BindView;
-import butterknife.Optional;
+import se.leap.bitmaskclient.BuildConfig;
import se.leap.bitmaskclient.R;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.base.views.ProviderHeaderView;
+import se.leap.bitmaskclient.tor.TorStatusObservable;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
+import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.getBootstrapProgress;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.getLastLogs;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.getLastSnowflakeLog;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.getLastTorLog;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.getStringForCurrentStatus;
/**
* Base Activity for configuration wizard activities
@@ -37,7 +55,7 @@ import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
* Created by fupduck on 09.01.18.
*/
-public abstract class ConfigWizardBaseActivity extends ButterKnifeActivity {
+public abstract class ConfigWizardBaseActivity extends ButterKnifeActivity implements Observer {
private static final String TAG = ConfigWizardBaseActivity.class.getName();
public static final float GUIDE_LINE_COMPACT_DELTA = 0.1f;
@@ -52,12 +70,50 @@ public abstract class ConfigWizardBaseActivity extends ButterKnifeActivity {
protected LinearLayout loadingScreen;
@Nullable
+ @BindView(R.id.btn_connection_detail)
+ protected AppCompatTextView connectionDetailBtn;
+
+ @Nullable
+ @BindView(R.id.connection_detail_header_container)
+ protected RelativeLayout connectionDetailHeaderContainer;
+
+ @Nullable
+ @BindView(R.id.connection_details_title)
+ protected AppCompatTextView connectionDetailsTitle;
+
+ @Nullable
+ @BindView(R.id.connection_detail_container)
+ protected RelativeLayout connectionDetailContainer;
+
+ @Nullable
+ @BindView(R.id.log_container)
+ protected RelativeLayout logsContainer;
+
+ @Nullable
+ @BindView(R.id.tor_state)
+ protected AppCompatTextView torState;
+
+ @Nullable
+ @BindView(R.id.snowflake_state)
+ protected AppCompatTextView snowflakeState;
+
+ @Nullable
+ @BindView(R.id.connection_detail_logs)
+ protected RecyclerView connectionDetailLogs;
+
+ private TorLogAdapter torLogAdapter;
+
+ @Nullable
@BindView(R.id.progressbar)
protected ProgressBar progressBar;
@Nullable
+ @BindView(R.id.progressbar_title)
+ protected AppCompatTextView progressbarTitle;
+
+ @Nullable
@BindView(R.id.progressbar_description)
- protected AppCompatTextView progressbarText;
+ protected AppCompatTextView progressbarDescription;
//Only tablet layouts have guidelines as they are based on a ConstraintLayout
@Nullable
@@ -142,12 +198,15 @@ public abstract class ConfigWizardBaseActivity extends ButterKnifeActivity {
protected void onPause() {
super.onPause();
isActivityShowing = false;
+ TorStatusObservable.getInstance().deleteObserver(this);
}
@Override
protected void onResume() {
super.onResume();
isActivityShowing = true;
+ TorStatusObservable.getInstance().addObserver(this);
+ setProgressbarDescription(getStringForCurrentStatus(this));
}
protected void restoreState(Bundle savedInstanceState) {
@@ -168,10 +227,64 @@ public abstract class ConfigWizardBaseActivity extends ButterKnifeActivity {
providerHeaderView.setTitle(providerHeaderText);
}
+ protected void hideConnectionDetails() {
+ if (loadingScreen == null) {
+ return;
+ }
+
+ connectionDetailHeaderContainer.setVisibility(GONE);
+ connectionDetailContainer.setVisibility(GONE);
+ logsContainer.setVisibility(GONE);
+ }
+
+ protected void showConnectionDetails() {
+ if (loadingScreen == null) {
+ return;
+ }
+ LinearLayoutManager layoutManager = new LinearLayoutManager(this);
+ connectionDetailLogs.setLayoutManager(layoutManager);
+ torLogAdapter = new TorLogAdapter(getLastLogs());
+ connectionDetailLogs.setAdapter(torLogAdapter);
+
+ connectionDetailLogs.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
+ super.onScrollStateChanged(recyclerView, newState);
+ if (newState != SCROLL_STATE_IDLE) {
+ torLogAdapter.postponeUpdate = true;
+ } else if (newState == SCROLL_STATE_IDLE && getFirstVisibleItemPosion() == 0) {
+ torLogAdapter.postponeUpdate = false;
+ }
+ }
+ });
+
+ snowflakeState.setText(getLastSnowflakeLog());
+ torState.setText(getLastTorLog());
+ connectionDetailBtn.setOnClickListener(v -> {
+ if (logsContainer.getVisibility() == VISIBLE) {
+ logsContainer.setVisibility(GONE);
+ connectionDetailContainer.setVisibility(GONE);
+ connectionDetailsTitle.setVisibility(GONE);
+ connectionDetailBtn.setText(R.string.show_connection_details);
+ } else {
+ logsContainer.setVisibility(VISIBLE);
+ connectionDetailContainer.setVisibility(VISIBLE);
+ connectionDetailsTitle.setVisibility(VISIBLE);
+ connectionDetailBtn.setText(R.string.hide_connection_details);
+ }
+ });
+ connectionDetailHeaderContainer.setVisibility(VISIBLE);
+ }
+
+ private int getFirstVisibleItemPosion() {
+ return ((LinearLayoutManager)connectionDetailLogs.getLayoutManager()).findFirstVisibleItemPosition();
+ }
+
protected void hideProgressBar() {
if (loadingScreen == null) {
return;
}
+ hideConnectionDetails();
loadingScreen.setVisibility(GONE);
content.setVisibility(VISIBLE);
}
@@ -184,11 +297,30 @@ public abstract class ConfigWizardBaseActivity extends ButterKnifeActivity {
loadingScreen.setVisibility(VISIBLE);
}
- protected void setProgressbarText(@StringRes int progressbarText) {
- if (this.progressbarText == null) {
+ protected void setProgressbarTitle(@StringRes int progressbarTitle) {
+ if (loadingScreen == null) {
+ return;
+ }
+ this.progressbarTitle.setText(progressbarTitle);
+ }
+
+ protected void setProgressbarDescription(String progressbarDescription) {
+ if (loadingScreen == null) {
+ return;
+ }
+ this.progressbarDescription.setText(progressbarDescription);
+ }
+
+ protected void setConfigProgress(int value) {
+ if (loadingScreen == null) {
return;
}
- this.progressbarText.setText(progressbarText);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ progressBar.setProgress(value);
+ } else {
+ progressBar.setProgress(value, true);
+ }
+ progressBar.setIndeterminate(value >= 100 || value < 0);
}
@@ -287,4 +419,76 @@ public abstract class ConfigWizardBaseActivity extends ButterKnifeActivity {
});
}
+ @Override
+ public void update(Observable o, Object arg) {
+ if (o instanceof TorStatusObservable) {
+ runOnUiThread(() -> {
+ if (TorStatusObservable.getStatus() != TorStatusObservable.TorStatus.OFF && loadingScreen != null) {
+ if (connectionDetailContainer.getVisibility() == GONE) {
+ showConnectionDetails();
+ } else {
+ setLogs(getLastTorLog(), getLastSnowflakeLog(), getLastLogs());
+ }
+ }
+ setProgressbarDescription(getStringForCurrentStatus(ConfigWizardBaseActivity.this));
+ setConfigProgress(getBootstrapProgress());
+ });
+ }
+ }
+
+ protected void setLogs(String torLog, String snowflakeLog, List<String> lastLogs) {
+ if (loadingScreen == null) {
+ return;
+ }
+ torLogAdapter.updateData(lastLogs);
+ torState.setText(torLog);
+ snowflakeState.setText(snowflakeLog);
+ }
+
+ static class TorLogAdapter extends RecyclerView.Adapter<TorLogAdapter.ViewHolder> {
+ private List<String> values;
+ private boolean postponeUpdate;
+
+ static class ViewHolder extends RecyclerView.ViewHolder {
+ public AppCompatTextView logTextLabel;
+ public View layout;
+
+ public ViewHolder(View v) {
+ super(v);
+ layout = v;
+ logTextLabel = v.findViewById(android.R.id.text1);
+ }
+ }
+
+ public void updateData(List<String> data) {
+ values = data;
+ if (!postponeUpdate) {
+ notifyDataSetChanged();
+ }
+ }
+
+ public TorLogAdapter(List<String> data) {
+ values = data;
+ }
+
+ @NonNull
+ @Override
+ public TorLogAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ LayoutInflater inflater = LayoutInflater.from(
+ parent.getContext());
+ View v = inflater.inflate(R.layout.v_log_item, parent, false);
+ return new TorLogAdapter.ViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(TorLogAdapter.ViewHolder holder, final int position) {
+ final String log = values.get(position);
+ holder.logTextLabel.setText(log);
+ }
+
+ @Override
+ public int getItemCount() {
+ return values.size();
+ }
+ }
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/LoginActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/LoginActivity.java
index a8bac6d8..9a5f31f2 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/LoginActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/LoginActivity.java
@@ -17,7 +17,7 @@ public class LoginActivity extends ProviderCredentialsBaseActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- setProgressbarText(R.string.logging_in);
+ setProgressbarTitle(R.string.logging_in);
setProviderHeaderLogo(R.drawable.logo);
setProviderHeaderText(R.string.login_to_profile);
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderSetupBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderSetupBaseActivity.java
index 40efd811..e429f776 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderSetupBaseActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderSetupBaseActivity.java
@@ -20,6 +20,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.util.Log;
+import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -29,15 +30,18 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.json.JSONException;
import org.json.JSONObject;
+import org.torproject.jni.TorService;
import se.leap.bitmaskclient.base.FragmentManagerEnhanced;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.providersetup.ProviderAPICommand;
-import se.leap.bitmaskclient.providersetup.ProviderDetailActivity;
import se.leap.bitmaskclient.providersetup.ProviderApiSetupBroadcastReceiver;
+import se.leap.bitmaskclient.providersetup.ProviderDetailActivity;
import se.leap.bitmaskclient.providersetup.ProviderManager;
import se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog;
import se.leap.bitmaskclient.providersetup.ProviderSetupInterface;
+import se.leap.bitmaskclient.tor.TorServiceCommand;
+import se.leap.bitmaskclient.tor.TorStatusObservable;
import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_PROVIDER_API_EVENT;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
@@ -51,13 +55,14 @@ import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.Provide
import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.SETTING_UP_PROVIDER;
import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.SHOWING_PROVIDER_DETAILS;
import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.SHOW_FAILED_DIALOG;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus.OFF;
/**
* Created by cyberta on 19.08.18.
*/
public abstract class ProviderSetupBaseActivity extends ConfigWizardBaseActivity implements ProviderSetupInterface, ProviderSetupFailedDialog.DownloadFailedDialogInterface {
- final public static String TAG = "PoviderSetupActivity";
+ final public static String TAG = "ProviderSetupActivity";
final private static String ACTIVITY_STATE = "ACTIVITY STATE";
final private static String REASON_TO_FAIL = "REASON TO FAIL";
@@ -86,11 +91,13 @@ public abstract class ProviderSetupBaseActivity extends ConfigWizardBaseActivity
showProgressBar();
} else if (PENDING_SHOW_FAILED_DIALOG == providerConfigState) {
showProgressBar();
+ hideConnectionDetails();
showDownloadFailedDialog();
} else if (SHOW_FAILED_DIALOG == providerConfigState) {
showProgressBar();
+ hideConnectionDetails();
} else if (SHOWING_PROVIDER_DETAILS == providerConfigState) {
- cancelSettingUpProvider();
+ cancelSettingUpProvider(false);
} else if (PENDING_SHOW_PROVIDER_DETAILS == providerConfigState) {
showProviderDetails();
}
@@ -142,7 +149,7 @@ public abstract class ProviderSetupBaseActivity extends ConfigWizardBaseActivity
}
@Override
- public void handleProviderSetupFailed(Bundle resultData) {
+ public void handleError(Bundle resultData) {
reasonToFail = resultData.getString(ERRORS);
showDownloadFailedDialog();
}
@@ -156,9 +163,7 @@ public abstract class ProviderSetupBaseActivity extends ConfigWizardBaseActivity
// -------- DownloadFailedDialogInterface ---v
@Override
public void cancelSettingUpProvider() {
- providerConfigState = PROVIDER_NOT_SET;
- provider = null;
- hideProgressBar();
+ cancelSettingUpProvider(true);
}
@Override
@@ -167,6 +172,16 @@ public abstract class ProviderSetupBaseActivity extends ConfigWizardBaseActivity
ProviderAPICommand.execute(this, UPDATE_PROVIDER_DETAILS, provider);
}
+ public void cancelSettingUpProvider(boolean stopTor) {
+ if (stopTor && TorStatusObservable.getStatus() != OFF) {
+ Log.d(TAG, "SHUTDOWN - cancelSettingUpProvider stopTor:" + stopTor);
+ TorServiceCommand.stopTorServiceAsync(this);
+ }
+ providerConfigState = PROVIDER_NOT_SET;
+ provider = null;
+ hideProgressBar();
+ }
+
protected void restoreState(Bundle savedInstanceState) {
super.restoreState(savedInstanceState);
if (savedInstanceState == null) {
@@ -196,7 +211,7 @@ public abstract class ProviderSetupBaseActivity extends ConfigWizardBaseActivity
/**
* Once selected a provider, this fragment offers the user to log in,
* use it anonymously (if possible)
- * or cancel his/her election pressing the back button.
+ * or cancel their selection pressing the back button.
*/
public void showProviderDetails() {
// show only if current activity is shown
@@ -218,6 +233,7 @@ public abstract class ProviderSetupBaseActivity extends ConfigWizardBaseActivity
public void showDownloadFailedDialog() {
try {
providerConfigState = SHOW_FAILED_DIALOG;
+ hideConnectionDetails();
FragmentTransaction fragmentTransaction = fragmentManager.removePreviousFragment(ProviderSetupFailedDialog.TAG);
DialogFragment newFragment;
try {
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SignupActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SignupActivity.java
index c0245845..16007a70 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SignupActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SignupActivity.java
@@ -37,7 +37,7 @@ public class SignupActivity extends ProviderCredentialsBaseActivity {
setProviderHeaderLogo(R.drawable.logo);
setProviderHeaderText(R.string.create_profile);
- setProgressbarText(R.string.signing_up);
+ setProgressbarTitle(R.string.signing_up);
setButtonText(R.string.signup_button);
passwordVerificationField.setVisibility(VISIBLE);
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java
index 2077a8b9..ea619263 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java
@@ -18,6 +18,7 @@
package se.leap.bitmaskclient.providersetup.connectivity;
import android.content.res.Resources;
+import android.net.LocalSocketAddress;
import android.os.Build;
import androidx.annotation.NonNull;
@@ -26,6 +27,9 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
@@ -50,6 +54,7 @@ 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_no_such_algorithm_exception_user_message;
import static se.leap.bitmaskclient.R.string.keyChainAccessError;
+import static se.leap.bitmaskclient.R.string.proxy;
import static se.leap.bitmaskclient.R.string.server_unreachable_message;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
import static se.leap.bitmaskclient.base.utils.ConfigHelper.getProviderFormattedString;
@@ -61,34 +66,35 @@ import static se.leap.bitmaskclient.base.utils.ConfigHelper.getProviderFormatted
public class OkHttpClientGenerator {
Resources resources;
+ private final static String PROXY_HOST = "127.0.0.1";
public OkHttpClientGenerator(/*SharedPreferences preferences,*/ Resources resources) {
this.resources = resources;
}
- public OkHttpClient initCommercialCAHttpClient(JSONObject initError) {
- return initHttpClient(initError, null);
+ public OkHttpClient initCommercialCAHttpClient(JSONObject initError, int proxyPort) {
+ return initHttpClient(initError, null, proxyPort);
}
- public OkHttpClient initSelfSignedCAHttpClient(String caCert, JSONObject initError) {
- return initHttpClient(initError, caCert);
+ public OkHttpClient initSelfSignedCAHttpClient(String caCert, int proxyPort, JSONObject initError) {
+ return initHttpClient(initError, caCert, proxyPort);
}
public OkHttpClient init() {
try {
- return createClient(null);
+ return createClient(null, -1);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
- private OkHttpClient initHttpClient(JSONObject initError, String certificate) {
+ private OkHttpClient initHttpClient(JSONObject initError, String certificate, int proxyPort) {
if (resources == null) {
return null;
}
try {
- return createClient(certificate);
+ return createClient(certificate, proxyPort);
} catch (IllegalArgumentException e) {
e.printStackTrace();
// TODO ca cert is invalid - show better error ?!
@@ -117,7 +123,7 @@ public class OkHttpClientGenerator {
return null;
}
- private OkHttpClient createClient(String certificate) throws Exception {
+ private OkHttpClient createClient(String certificate, int proxyPort) throws Exception {
TLSCompatSocketFactory sslCompatFactory;
ConnectionSpec spec = getConnectionSpec();
OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
@@ -131,6 +137,9 @@ public class OkHttpClientGenerator {
clientBuilder.cookieJar(getCookieJar())
.connectionSpecs(Collections.singletonList(spec));
clientBuilder.dns(new DnsResolver());
+ if (proxyPort != -1) {
+ clientBuilder.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(PROXY_HOST, proxyPort)));
+ }
return clientBuilder.build();
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java b/app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java
new file mode 100644
index 00000000..764d5f06
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java
@@ -0,0 +1,178 @@
+package se.leap.bitmaskclient.tor;
+/**
+ * Copyright (c) 2021 LEAP Encryption Access Project and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+import android.content.Context;
+import android.os.FileObserver;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.torproject.jni.ClientTransportPluginInterface;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.ref.WeakReference;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Scanner;
+import java.util.Vector;
+import java.util.concurrent.TimeoutException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import IPtProxy.IPtProxy;
+
+public class ClientTransportPlugin implements ClientTransportPluginInterface {
+ public static String TAG = ClientTransportPlugin.class.getSimpleName();
+
+ private HashMap<String, String> mFronts;
+ private final WeakReference<Context> contextRef;
+ private long snowflakePort = -1;
+ private FileObserver logFileObserver;
+ private static final Pattern SNOWFLAKE_LOG_TIMESTAMP_PATTERN = Pattern.compile("((19|2[0-9])[0-9]{2}\\/\\d{1,2}\\/\\d{1,2} \\d{1,2}:\\d{1,2}:\\d{1,2}) ([\\S|\\s]+)");
+
+ public ClientTransportPlugin(Context context) {
+ this.contextRef = new WeakReference<>(context);
+ loadCdnFronts(context);
+ }
+
+ @Override
+ public void start() {
+ Context context = contextRef.get();
+ if (context == null) {
+ return;
+ }
+ File logfile = new File(context.getApplicationContext().getCacheDir(), "snowflake.log");
+ Log.d(TAG, "logfile at " + logfile.getAbsolutePath());
+ try {
+ if (logfile.exists()) {
+ logfile.delete();
+ }
+ logfile.createNewFile();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ //this is using the current, default Tor snowflake infrastructure
+ String target = getCdnFront("snowflake-target");
+ String front = getCdnFront("snowflake-front");
+ String stunServer = getCdnFront("snowflake-stun");
+ Log.d(TAG, "startSnowflake. target: " + target + ", front:" + front + ", stunServer" + stunServer);
+ snowflakePort = IPtProxy.startSnowflake( stunServer, target, front, logfile.getAbsolutePath(), false, false, true, 5);
+ Log.d(TAG, "startSnowflake running on port: " + snowflakePort);
+ watchLogFile(logfile);
+ }
+
+ private void watchLogFile(File logfile) {
+ final Vector<String> lastBuffer = new Vector<>();
+ logFileObserver = new FileObserver(logfile.getAbsolutePath()) {
+ @Override
+ public void onEvent(int event, @Nullable String name) {
+ if (FileObserver.MODIFY == event) {
+ try (Scanner scanner = new Scanner(logfile)) {
+ Vector<String> currentBuffer = new Vector<>();
+ while (scanner.hasNextLine()) {
+ currentBuffer.add(scanner.nextLine());
+ }
+ if (lastBuffer.size() < currentBuffer.size()) {
+ int startIndex = lastBuffer.size() > 0 ? lastBuffer.size() - 1 : 0;
+ int endIndex = currentBuffer.size() - 1;
+ Collection<String> newMessages = currentBuffer.subList(startIndex, endIndex);
+ for (String message : newMessages) {
+ logSnowflakeMessage(message);
+ }
+ lastBuffer.addAll(newMessages);
+ }
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ };
+ logFileObserver.startWatching();
+ }
+
+ @Override
+ public void stop() {
+ IPtProxy.stopSnowflake();
+ try {
+ TorStatusObservable.waitUntil(this::isSnowflakeOff, 10);
+ } catch (InterruptedException | TimeoutException e) {
+ e.printStackTrace();
+ }
+ snowflakePort = -1;
+ logFileObserver.stopWatching();
+ }
+
+ private boolean isSnowflakeOff() {
+ return TorStatusObservable.getSnowflakeStatus() == TorStatusObservable.SnowflakeStatus.OFF;
+ }
+
+ @Override
+ public String getTorrc() {
+ return "UseBridges 1\n" +
+ "ClientTransportPlugin snowflake socks5 127.0.0.1:" + snowflakePort + "\n" +
+ "Bridge snowflake 192.0.2.3:1";
+ }
+
+ private void loadCdnFronts(Context context) {
+ if (mFronts == null) {
+ mFronts = new HashMap<>();
+ }
+ try {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(context.getAssets().open("fronts")));
+ String line;
+ while (true) {
+ line = reader.readLine();
+ if (line == null) break;
+ String[] front = line.split(" ");
+ mFronts.put(front[0], front[1]);
+ Log.d(TAG, "front: " + front[0] + ", " + front[1]);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Nullable
+ private String getCdnFront(String service) {
+ if (mFronts != null) {
+ return mFronts.get(service);
+ }
+ return null;
+ }
+
+ private void logSnowflakeMessage(String message) {
+ Matcher matcher = SNOWFLAKE_LOG_TIMESTAMP_PATTERN.matcher(message);
+ if (matcher.matches()) {
+ try {
+ String strippedString = matcher.group(3).trim();
+ if (strippedString.length() > 0) {
+ TorStatusObservable.logSnowflakeMessage(contextRef.get(), strippedString);
+ }
+ } catch (IndexOutOfBoundsException | IllegalStateException e) {
+ e.printStackTrace();
+ }
+ } else {
+ TorStatusObservable.logSnowflakeMessage(contextRef.get(), message);
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/tor/TorNotificationManager.java b/app/src/main/java/se/leap/bitmaskclient/tor/TorNotificationManager.java
new file mode 100644
index 00000000..3f3fbf4f
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/tor/TorNotificationManager.java
@@ -0,0 +1,128 @@
+package se.leap.bitmaskclient.tor;
+/**
+ * Copyright (c) 2021 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/>.
+ */
+
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.Build;
+
+import androidx.core.app.NotificationCompat;
+
+import se.leap.bitmaskclient.R;
+
+public class TorNotificationManager {
+ public final static int TOR_SERVICE_NOTIFICATION_ID = 10;
+ static final String NOTIFICATION_CHANNEL_NEWSTATUS_ID = "bitmask_tor_service_news";
+ private long lastNotificationTime = 0;
+ // debounce timeout in milliseconds
+ private final static long NOTIFICATION_DEBOUNCE_TIME = 500;
+
+
+ public TorNotificationManager() {}
+
+
+ public static Notification buildTorForegroundNotification(Context context) {
+ NotificationManager notificationManager = initNotificationManager(context);
+ if (notificationManager == null) {
+ return null;
+ }
+ NotificationCompat.Builder notificationBuilder = initNotificationBuilderDefaults(context);
+ return notificationBuilder
+ .setSmallIcon(R.drawable.ic_bridge_36)
+ .setWhen(System.currentTimeMillis())
+ .setContentText(context.getString(R.string.tor_started)).build();
+ }
+
+ public void buildTorNotification(Context context, String state, String message, int progress) {
+ if (shouldDropNotification()) {
+ return;
+ }
+ NotificationManager notificationManager = initNotificationManager(context);
+ if (notificationManager == null) {
+ return;
+ }
+ NotificationCompat.Builder notificationBuilder = initNotificationBuilderDefaults(context);
+ notificationBuilder
+ .setSmallIcon(R.drawable.ic_bridge_36)
+ .setWhen(System.currentTimeMillis())
+ .setStyle(new NotificationCompat.BigTextStyle().
+ setBigContentTitle(state).
+ bigText(message))
+ .setTicker(message)
+ .setContentTitle(state)
+ .setOnlyAlertOnce(true)
+ .setContentText(message);
+ if (progress > 0) {
+ notificationBuilder.setProgress(100, progress, false);
+ }
+ notificationManager.notify(TOR_SERVICE_NOTIFICATION_ID, notificationBuilder.build());
+ }
+
+ private boolean shouldDropNotification() {
+ long now = System.currentTimeMillis();
+ if (now - lastNotificationTime < NOTIFICATION_DEBOUNCE_TIME) {
+ return true;
+ }
+ lastNotificationTime = now;
+ return false;
+ }
+
+
+ private static NotificationManager initNotificationManager(Context context) {
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ if (notificationManager == null) {
+ return null;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ createNotificationChannel(context, notificationManager);
+ }
+ return notificationManager;
+ }
+
+ @TargetApi(26)
+ private static void createNotificationChannel(Context context, NotificationManager notificationManager) {
+ String appName = context.getString(R.string.app_name);
+ CharSequence name = context.getString(R.string.channel_name_tor_service, appName);
+ String description = context.getString(R.string.channel_description_tor_service, appName);
+ NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_NEWSTATUS_ID,
+ name,
+ NotificationManager.IMPORTANCE_LOW);
+ channel.setSound(null, null);
+ channel.setDescription(description);
+ notificationManager.createNotificationChannel(channel);
+ }
+
+ private static NotificationCompat.Builder initNotificationBuilderDefaults(Context context) {
+ NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_NEWSTATUS_ID);
+ notificationBuilder.
+ setDefaults(Notification.DEFAULT_ALL).
+ setLocalOnly(true).
+ setAutoCancel(false);
+ return notificationBuilder;
+ }
+
+ public void cancelNotifications(Context context) {
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ if (notificationManager == null) {
+ return;
+ }
+ notificationManager.cancel(TOR_SERVICE_NOTIFICATION_ID);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/tor/TorServiceCommand.java b/app/src/main/java/se/leap/bitmaskclient/tor/TorServiceCommand.java
new file mode 100644
index 00000000..461ee356
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/tor/TorServiceCommand.java
@@ -0,0 +1,138 @@
+package se.leap.bitmaskclient.tor;
+/**
+ * Copyright (c) 2021 LEAP Encryption Access Project and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+import android.app.Notification;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.WorkerThread;
+
+import org.torproject.jni.TorService;
+
+import java.util.concurrent.TimeoutException;
+
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+
+import static se.leap.bitmaskclient.tor.TorNotificationManager.TOR_SERVICE_NOTIFICATION_ID;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.waitUntil;
+
+public class TorServiceCommand {
+
+
+ private static String TAG = TorServiceCommand.class.getSimpleName();
+
+ // we bind the service before starting it as foreground service so that we avoid startForeground related RemoteExceptions
+ @WorkerThread
+ public static boolean startTorService(Context context, String action) throws InterruptedException {
+ Log.d(TAG, "startTorService");
+ try {
+ waitUntil(TorServiceCommand::isNotCancelled, 30);
+ } catch (TimeoutException e) {
+ e.printStackTrace();
+ }
+ TorServiceConnection torServiceConnection = initTorServiceConnection(context);
+ Log.d(TAG, "startTorService foreground: " + (torServiceConnection != null));
+ boolean startedForeground = false;
+ if (torServiceConnection == null) {
+ return startedForeground;
+ }
+
+ try {
+ Intent torServiceIntent = new Intent(context, TorService.class);
+ torServiceIntent.setAction(action);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ Notification notification = TorNotificationManager.buildTorForegroundNotification(context.getApplicationContext());
+ //noinspection NewApi
+ context.getApplicationContext().startForegroundService(torServiceIntent);
+ torServiceConnection.getService().startForeground(TOR_SERVICE_NOTIFICATION_ID, notification);
+ } else {
+ context.getApplicationContext().startService(torServiceIntent);
+ }
+ startedForeground = true;
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ }
+
+ if (torServiceConnection != null) {
+ torServiceConnection.close();
+ }
+
+ return startedForeground;
+ }
+
+ @WorkerThread
+ public static void stopTorService(Context context) {
+ if (TorStatusObservable.getStatus() == TorStatusObservable.TorStatus.OFF) {
+ return;
+ }
+ TorStatusObservable.markCancelled();
+
+ try {
+ Intent torServiceIntent = new Intent(context, TorService.class);
+ torServiceIntent.setAction(TorService.ACTION_STOP);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ //noinspection NewApi
+ context.getApplicationContext().startService(torServiceIntent);
+ } else {
+ context.getApplicationContext().startService(torServiceIntent);
+ }
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static void stopTorServiceAsync(Context context) {
+ TorStatusObservable.markCancelled();
+ new Thread(() -> stopTorService(context)).start();
+ }
+
+ @WorkerThread
+ public static int getHttpTunnelPort(Context context) {
+ try {
+ TorServiceConnection torServiceConnection = initTorServiceConnection(context);
+ if (torServiceConnection != null) {
+ int tunnelPort = torServiceConnection.getService().getHttpTunnelPort();
+ torServiceConnection.close();
+ return tunnelPort;
+ }
+ } catch (InterruptedException | IllegalStateException e) {
+ e.printStackTrace();
+ }
+ return -1;
+ }
+
+ private static boolean isNotCancelled() {
+ return !TorStatusObservable.isCancelled();
+ }
+
+
+ private static TorServiceConnection initTorServiceConnection(Context context) throws InterruptedException, IllegalStateException {
+ Log.d(TAG, "initTorServiceConnection");
+ if (PreferenceHelper.getUseTor(context)) {
+ Log.d(TAG, "serviceConnection is still null");
+ if (!TorService.hasClientTransportPlugin()) {
+ TorService.setClientTransportPlugin(new ClientTransportPlugin(context.getApplicationContext()));
+ }
+ return new TorServiceConnection(context);
+ }
+ return null;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/tor/TorServiceConnection.java b/app/src/main/java/se/leap/bitmaskclient/tor/TorServiceConnection.java
new file mode 100644
index 00000000..dbfce2b5
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/tor/TorServiceConnection.java
@@ -0,0 +1,88 @@
+package se.leap.bitmaskclient.tor;
+/**
+ * Copyright (c) 2021 LEAP Encryption Access Project and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.util.Log;
+
+import androidx.annotation.WorkerThread;
+
+import org.torproject.jni.TorService;
+
+import java.io.Closeable;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import se.leap.bitmaskclient.providersetup.ProviderAPI;
+
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.ensureNotOnMainThread;
+
+public class TorServiceConnection implements Closeable {
+ private static final String TAG = TorServiceConnection.class.getSimpleName();
+ private final Context context;
+ private ServiceConnection serviceConnection;
+ private TorService torService;
+
+ @WorkerThread
+ public TorServiceConnection(Context context) throws InterruptedException, IllegalStateException {
+ this.context = context;
+ ensureNotOnMainThread(context);
+ initSynchronizedServiceConnection(context);
+ }
+
+ @Override
+ public void close() {
+ context.unbindService(serviceConnection);
+ }
+
+ private void initSynchronizedServiceConnection(final Context context) throws InterruptedException {
+ Log.d(TAG, "initSynchronizedServiceConnection");
+ final BlockingQueue<TorService> blockingQueue = new LinkedBlockingQueue<>(1);
+ this.serviceConnection = new ServiceConnection() {
+ volatile boolean mConnectedAtLeastOnce = false;
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (!mConnectedAtLeastOnce) {
+ mConnectedAtLeastOnce = true;
+ Log.d(TAG, "onServiceConnected");
+ try {
+ TorService.LocalBinder binder = (TorService.LocalBinder) service;
+ blockingQueue.put(binder.getService());
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ torService = null;
+ }
+ };
+ Intent intent = new Intent(context, TorService.class);
+ context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
+ torService = blockingQueue.take();
+ }
+
+ public TorService getService() {
+ return torService;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java b/app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java
new file mode 100644
index 00000000..3c280b9c
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java
@@ -0,0 +1,290 @@
+package se.leap.bitmaskclient.tor;
+/**
+ * Copyright (c) 2021 LEAP Encryption Access Project and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import java.util.Observable;
+import java.util.Observer;
+import java.util.Vector;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import se.leap.bitmaskclient.R;
+
+public class TorStatusObservable extends Observable {
+
+ private static final String TAG = TorStatusObservable.class.getSimpleName();
+
+ public interface StatusCondition {
+ boolean met();
+ }
+
+ public enum TorStatus {
+ ON,
+ OFF,
+ STARTING,
+ STOPPING
+ }
+
+ public enum SnowflakeStatus {
+ ON,
+ OFF
+ }
+
+ // indicates if the user has cancelled Tor, the actual TorStatus can still be different until
+ // the TorService has sent the shutdown signal
+ private boolean cancelled = false;
+
+ public static final String LOG_TAG_TOR = "[TOR]";
+ public static final String LOG_TAG_SNOWFLAKE = "[SNOWFLAKE]";
+ public static final String SNOWFLAKE_STARTED = "--- Starting Snowflake Client ---";
+ public static final String SNOWFLAKE_STOPPED_COLLECTING = "---- SnowflakeConn: end collecting snowflakes ---";
+ public static final String SNOWFLAKE_COPY_LOOP_STOPPED = "copy loop ended";
+ public static final String SNOWFLAKE_SOCKS_ERROR = "SOCKS accept error";
+
+ private static TorStatusObservable instance;
+ private TorStatus status = TorStatus.OFF;
+ private SnowflakeStatus snowflakeStatus = SnowflakeStatus.OFF;
+ private final TorNotificationManager torNotificationManager;
+ private String lastError;
+ private String lastTorLog = "";
+ private String lastSnowflakeLog = "";
+ private int port = -1;
+ private int bootstrapPercent = -1;
+ private Vector<String> lastLogs = new Vector<>(100);
+
+ private TorStatusObservable() {
+ torNotificationManager = new TorNotificationManager();
+ }
+
+ public static TorStatusObservable getInstance() {
+ if (instance == null) {
+ instance = new TorStatusObservable();
+ }
+ return instance;
+ }
+
+ public static TorStatus getStatus() {
+ return getInstance().status;
+ }
+
+ public static SnowflakeStatus getSnowflakeStatus() {
+ return getInstance().snowflakeStatus;
+ }
+
+ /**
+ * Waits on the current Thread until a certain tor/snowflake status has been reached
+ * @param condition defines when wait should be interrupted
+ * @param timeout Timout in seconds
+ * @throws InterruptedException if thread was interrupted while waiting
+ * @throws TimeoutException thrown if timeout was reached
+ * @return true return value only needed to mock this method call
+ */
+ public static boolean waitUntil(StatusCondition condition, int timeout) throws InterruptedException, TimeoutException {
+ CountDownLatch countDownLatch = new CountDownLatch(1);
+ final AtomicBoolean conditionMet = new AtomicBoolean(false);
+ Observer observer = (o, arg) -> {
+ if (condition.met()) {
+ countDownLatch.countDown();
+ conditionMet.set(true);
+ }
+ };
+ if (condition.met()) {
+ // no need to wait
+ return true;
+ }
+ getInstance().addObserver(observer);
+ countDownLatch.await(timeout, TimeUnit.SECONDS);
+ getInstance().deleteObserver(observer);
+ if (!conditionMet.get()) {
+ throw new TimeoutException("Status condition not met within " + timeout + "s.");
+ }
+ return true;
+ }
+
+ public static void logSnowflakeMessage(Context context, String message) {
+ addLog(message);
+ getInstance().lastSnowflakeLog = message;
+ if (getInstance().status != TorStatus.OFF) {
+ getInstance().torNotificationManager.buildTorNotification(context, getStringForCurrentStatus(context), getNotificationLog(), getBootstrapProgress());
+ }
+ //TODO: implement proper state signalling in IPtProxy
+ if (SNOWFLAKE_STARTED.equals(message.trim())) {
+ Log.d(TAG, "snowflakeStatus ON");
+ getInstance().snowflakeStatus = SnowflakeStatus.ON;
+ } else if (SNOWFLAKE_STOPPED_COLLECTING.equals(message.trim()) ||
+ SNOWFLAKE_COPY_LOOP_STOPPED.equals(message.trim()) ||
+ message.trim().contains(SNOWFLAKE_SOCKS_ERROR)) {
+ Log.d(TAG, "snowflakeStatus OFF");
+ getInstance().snowflakeStatus = SnowflakeStatus.OFF;
+ }
+ instance.setChanged();
+ instance.notifyObservers();
+ }
+
+ private static String getNotificationLog() {
+ String snowflakeIcon = new String(Character.toChars(0x2744));
+ String snowflakeLog = getInstance().lastSnowflakeLog;
+ // we don't want to show the response json in the notification
+ if (snowflakeLog != null && snowflakeLog.contains("Received answer: {")) {
+ snowflakeLog = "Received Answer.";
+ }
+ return "Tor: " + getInstance().lastTorLog + "\n" +
+ snowflakeIcon + ": " + snowflakeLog;
+ }
+
+ public static int getBootstrapProgress() {
+ return getInstance().status == TorStatus.STARTING ? getInstance().bootstrapPercent : -1;
+ }
+
+ private static void addLog(String message) {
+ if (instance.lastLogs.size() > 100) {
+ instance.lastLogs.remove(99);
+ }
+ instance.lastLogs.add(0, message.trim());
+ }
+
+ public static void updateState(Context context, String status) {
+ updateState(context,status, -1, null);
+ }
+
+ public static void updateState(Context context, String status, int bootstrapPercent, @Nullable String logKey) {
+ try {
+ Log.d(TAG, "update tor state: " + status + " " + bootstrapPercent + " "+ logKey);
+ getInstance().status = TorStatus.valueOf(status);
+ if (bootstrapPercent != -1) {
+ getInstance().bootstrapPercent = bootstrapPercent;
+ }
+
+ if (getInstance().status == TorStatus.OFF) {
+ getInstance().torNotificationManager.cancelNotifications(context);
+ getInstance().cancelled = false;
+ getInstance().port = -1;
+ } else {
+ if (logKey != null) {
+ getInstance().lastTorLog = getStringFor(context, logKey);
+ addLog(getInstance().lastTorLog);
+ }
+ getInstance().torNotificationManager.buildTorNotification(context, getStringForCurrentStatus(context), getNotificationLog(), getBootstrapProgress());
+ }
+
+ instance.setChanged();
+ instance.notifyObservers();
+
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private static String getStringFor(Context context, String key) {
+ switch (key) {
+ case "conn_pt":
+ return context.getString(R.string.log_conn_pt);
+ case "conn_done_pt":
+ return context.getString(R.string.log_conn_done_pt);
+ case "conn_done":
+ return context.getString(R.string.log_conn_done);
+ case "handshake":
+ return context.getString(R.string.log_handshake);
+ case "handshake_done":
+ return context.getString(R.string.log_handshake_done);
+ case "onehop_create":
+ return context.getString(R.string.log_onehop_create);
+ case "requesting_status":
+ return context.getString(R.string.log_requesting_status);
+ case "loading_status":
+ return context.getString(R.string.log_loading_status);
+ case "loading_keys":
+ return context.getString(R.string.log_loading_keys);
+ case "requesting_descriptors":
+ return context.getString(R.string.log_requesting_desccriptors);
+ case "loading_descriptors":
+ return context.getString(R.string.log_loading_descriptors);
+ case "enough_dirinfo":
+ return context.getString(R.string.log_enough_dirinfo);
+ case "ap_handshake_done":
+ return context.getString(R.string.log_ap_handshake_done);
+ case "circuit_create":
+ return context.getString(R.string.log_circuit_create);
+ case "done":
+ return context.getString(R.string.log_done);
+ default:
+ return key;
+ }
+ }
+
+ public static void setLastError(String error) {
+ getInstance().lastError = error;
+ instance.setChanged();
+ instance.notifyObservers();
+ }
+
+ public static void setProxyPort(int port) {
+ getInstance().port = port;
+ instance.setChanged();
+ instance.notifyObservers();
+ }
+
+ public static int getProxyPort() {
+ return getInstance().port;
+ }
+
+
+ @Nullable
+ public static String getLastTorLog() {
+ return getInstance().lastTorLog;
+ }
+
+ @Nullable
+ public static String getLastSnowflakeLog() {
+ return getInstance().lastSnowflakeLog;
+ }
+
+ public static Vector<String> getLastLogs() {
+ return getInstance().lastLogs;
+ }
+
+ public static String getStringForCurrentStatus(Context context) {
+ switch (getInstance().status) {
+ case ON:
+ return context.getString(R.string.tor_started);
+ case STARTING:
+ return context.getString(R.string.tor_starting);
+ case STOPPING:
+ return context.getString(R.string.tor_stopping);
+ case OFF:
+ break;
+ }
+ return "";
+ }
+
+ public static void markCancelled() {
+ if (!getInstance().cancelled) {
+ getInstance().cancelled = true;
+ getInstance().notifyObservers();
+ }
+ }
+
+ public static boolean isCancelled() {
+ return getInstance().cancelled;
+ }
+}
diff --git a/app/src/main/res/drawable/ic_snowflake.png b/app/src/main/res/drawable/ic_snowflake.png
new file mode 100644
index 00000000..992662ee
--- /dev/null
+++ b/app/src/main/res/drawable/ic_snowflake.png
Binary files differ
diff --git a/app/src/main/res/drawable/ic_tor.png b/app/src/main/res/drawable/ic_tor.png
new file mode 100644
index 00000000..a5f9ae89
--- /dev/null
+++ b/app/src/main/res/drawable/ic_tor.png
Binary files differ
diff --git a/app/src/main/res/drawable/v_vertical_gradient.xml b/app/src/main/res/drawable/v_vertical_gradient.xml
new file mode 100644
index 00000000..877634b5
--- /dev/null
+++ b/app/src/main/res/drawable/v_vertical_gradient.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <gradient
+ android:angle="270"
+ android:startColor="#00FFFFFF"
+ android:endColor="#FFFFFFFF"
+ android:type="linear" />
+</shape>
diff --git a/app/src/main/res/layout-xlarge/v_loading_screen.xml b/app/src/main/res/layout-xlarge/v_loading_screen.xml
index a002665f..24b44f9a 100644
--- a/app/src/main/res/layout-xlarge/v_loading_screen.xml
+++ b/app/src/main/res/layout-xlarge/v_loading_screen.xml
@@ -1,11 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:id="@+id/loading_screen"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
- android:visibility="gone">
+ android:visibility="gone"
+ tools:visibility="visible"
+ >
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="32dp"
@@ -18,7 +21,7 @@
/>
<androidx.appcompat.widget.AppCompatTextView
- android:id="@+id/progressbar_description"
+ android:id="@+id/progressbar_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fadingEdge="horizontal"
@@ -29,6 +32,21 @@
android:layout_marginBottom="@dimen/standard_margin"
/>
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/progressbar_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fadingEdge="horizontal"
+ android:maxLines="2"
+ android:text="@string/configuring_provider"
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Small"
+ android:layout_marginTop="@dimen/standard_margin"
+ android:layout_marginBottom="@dimen/standard_margin"
+ tools:text="test"
+ tools:visibility="visible"
+
+ />
+
<ProgressBar
android:id="@+id/progressbar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
@@ -38,4 +56,158 @@
android:layout_marginTop="@dimen/standard_margin"
/>
+ <RelativeLayout
+ android:id="@+id/connection_detail_header_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/stdpadding"
+ >
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/btn_connection_detail"
+ android:layout_marginTop="@dimen/stdpadding"
+ android:paddingLeft="@dimen/stdpadding"
+ android:paddingStart="@dimen/stdpadding"
+ android:paddingEnd="@dimen/stdpadding"
+ android:paddingRight="@dimen/stdpadding"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:text="@string/show_connection_details"
+ android:textColor="@color/colorPrimaryDark"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:visibility="visible"
+ tools:text="@string/hide_connection_details"
+ />
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/connection_details_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@id/btn_connection_detail"
+ android:text="@string/connection_details"
+ android:gravity="start"
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
+ android:paddingBottom="@dimen/stdpadding"
+ android:paddingStart="4dp"
+ android:paddingLeft="4dp"
+ android:paddingEnd="4dp"
+ android:paddingRight="4dp"
+ tools:visibility="visible"
+ android:visibility="gone"
+ />
+ </RelativeLayout>
+ <RelativeLayout
+ android:id="@+id/connection_detail_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ tools:visibility="visible"
+ >
+
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/tor_icon"
+ android:layout_width="35dp"
+ android:layout_height="35dp"
+ android:src="@drawable/ic_tor"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_marginBottom="@dimen/stdpadding"
+ />
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/tor_state"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fadingEdge="horizontal"
+ android:maxLines="2"
+ android:text="@string/configuring_provider"
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Small"
+ android:layout_alignBottom="@id/tor_icon"
+ android:layout_toEndOf="@id/tor_icon"
+ android:layout_toRightOf="@+id/tor_icon"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentEnd="true"
+ android:gravity="bottom"
+ tools:text="test 12321 123 \n sdf,sdf,m\nn 123 "
+ android:ellipsize="end"
+ tools:visibility="visible"
+ />
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/snowflake_icon"
+ android:layout_width="35dp"
+ android:layout_height="35dp"
+ android:src="@drawable/ic_snowflake"
+ android:layout_below="@id/tor_icon"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_marginBottom="@dimen/stdpadding"
+ />
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/snowflake_state"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/tor_state"
+ android:fadingEdge="horizontal"
+ android:maxLines="2"
+ android:text="@string/configuring_provider"
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Small"
+ android:layout_alignTop="@id/snowflake_icon"
+ android:layout_alignBottom="@+id/snowflake_icon"
+ android:layout_toEndOf="@+id/snowflake_icon"
+ android:layout_toRightOf="@+id/snowflake_icon"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:paddingBottom="1dp"
+ android:gravity="bottom"
+ tools:text="test \n another \n and a third \n blkud"
+ android:ellipsize="end"
+ tools:visibility="visible"
+ />
+ </RelativeLayout>
+ <RelativeLayout
+ android:id="@+id/log_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ tools:visibility="visible"
+ >
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/connection_detail_logs_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/log_fragment_title"
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
+ android:layout_alignParentTop="true"
+ android:paddingStart="4dp"
+ android:paddingLeft="4dp"
+ android:paddingEnd="4dp"
+ android:paddingRight="4dp"
+ android:paddingTop="@dimen/stdpadding"
+ android:paddingBottom="@dimen/stdpadding"
+ />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/connection_detail_logs"
+ android:layout_below="@+id/connection_detail_logs_title"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:listitem="@layout/v_log_item"
+ android:isScrollContainer="false"
+ />
+
+ <ImageView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/connection_detail_logs"
+ android:layout_alignTop="@id/connection_detail_logs"
+ android:src="@drawable/v_vertical_gradient"
+ android:layout_marginTop="200dp"
+ android:importantForAccessibility="no"
+ />
+ </RelativeLayout>
+
</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/v_add_provider.xml b/app/src/main/res/layout/v_add_provider.xml
deleted file mode 100644
index afcae4af..00000000
--- a/app/src/main/res/layout/v_add_provider.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/loading_screen"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:visibility="gone">
-
- <androidx.appcompat.widget.AppCompatImageView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:adjustViewBounds="true"
- app:tint="@color/colorPrimary"
- app:srcCompat="@drawable/action_history"
- android:layout_marginTop="@dimen/loading_screen_icon_vertical_margin"
- android:layout_marginBottom="@dimen/loading_screen_icon_vertical_margin"
- />
-
- <androidx.appcompat.widget.AppCompatTextView
- android:id="@+id/progressbar_description"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:fadingEdge="horizontal"
- android:singleLine="true"
- android:text="@string/introduce_new_provider"
- android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
- android:layout_marginTop="@dimen/standard_margin"
- android:layout_marginBottom="@dimen/standard_margin"
- />
-
- <ProgressBar
- android:id="@+id/progressbar"
- style="@style/Widget.AppCompat.ProgressBar.Horizontal"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:indeterminate="true"
- android:layout_marginTop="@dimen/standard_margin"
- />
-
-</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/v_loading_screen.xml b/app/src/main/res/layout/v_loading_screen.xml
index a3498e97..26ab25cc 100644
--- a/app/src/main/res/layout/v_loading_screen.xml
+++ b/app/src/main/res/layout/v_loading_screen.xml
@@ -1,11 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:id="@+id/loading_screen"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
- android:visibility="gone">
+ android:visibility="gone"
+ tools:visibility="visible"
+ >
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
@@ -18,7 +21,7 @@
/>
<androidx.appcompat.widget.AppCompatTextView
- android:id="@+id/progressbar_description"
+ android:id="@+id/progressbar_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fadingEdge="horizontal"
@@ -29,6 +32,20 @@
android:layout_marginBottom="@dimen/standard_margin"
/>
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/progressbar_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fadingEdge="horizontal"
+ android:maxLines="2"
+ android:text="@string/configuring_provider"
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Small"
+ android:layout_marginTop="@dimen/standard_margin"
+ android:layout_marginBottom="@dimen/standard_margin"
+ tools:text="test"
+ tools:visibility="visible"
+ />
+
<ProgressBar
android:id="@+id/progressbar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
@@ -38,4 +55,158 @@
android:layout_marginTop="@dimen/standard_margin"
/>
+
+ <RelativeLayout
+ android:id="@+id/connection_detail_header_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/stdpadding"
+ >
+
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/btn_connection_detail"
+ android:layout_marginTop="@dimen/stdpadding"
+ android:paddingLeft="@dimen/stdpadding"
+ android:paddingStart="@dimen/stdpadding"
+ android:paddingEnd="@dimen/stdpadding"
+ android:paddingRight="@dimen/stdpadding"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:text="@string/show_connection_details"
+ android:textColor="@color/colorPrimaryDark"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:visibility="visible"
+ />
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/connection_details_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@id/btn_connection_detail"
+ android:text="@string/connection_details"
+ android:gravity="start"
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
+ android:paddingBottom="@dimen/stdpadding"
+ android:paddingStart="4dp"
+ android:paddingLeft="4dp"
+ android:paddingEnd="4dp"
+ android:paddingRight="4dp"
+ android:visibility="gone"
+ />
+ </RelativeLayout>
+
+ <RelativeLayout
+ android:id="@+id/connection_detail_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ tools:visibility="visible"
+ >
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/tor_icon"
+ android:layout_width="35dp"
+ android:layout_height="35dp"
+ android:src="@drawable/ic_tor"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_marginBottom="@dimen/stdpadding"
+ />
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/tor_state"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fadingEdge="horizontal"
+ android:maxLines="2"
+ android:text="@string/configuring_provider"
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Small"
+ android:layout_alignBottom="@id/tor_icon"
+ android:layout_toEndOf="@id/tor_icon"
+ android:layout_toRightOf="@+id/tor_icon"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentEnd="true"
+ android:gravity="bottom"
+ tools:text="test 12321 123 \n sdf,sdf,m\nn 123 "
+ android:ellipsize="end"
+
+ tools:visibility="visible"
+ />
+
+ <androidx.appcompat.widget.AppCompatImageView
+ android:id="@+id/snowflake_icon"
+ android:layout_width="35dp"
+ android:layout_height="35dp"
+ android:src="@drawable/ic_snowflake"
+ android:layout_below="@id/tor_icon"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_marginBottom="@dimen/stdpadding"
+ />
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/snowflake_state"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/tor_state"
+ android:fadingEdge="horizontal"
+ android:maxLines="2"
+ android:text="@string/configuring_provider"
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Small"
+ android:layout_alignTop="@id/snowflake_icon"
+ android:layout_alignBottom="@+id/snowflake_icon"
+ android:layout_toEndOf="@+id/snowflake_icon"
+ android:layout_toRightOf="@+id/snowflake_icon"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:paddingBottom="1dp"
+ android:gravity="bottom"
+ tools:text="test \n another \n and a third \n blkud"
+ android:ellipsize="end"
+ tools:visibility="visible"
+ />
+ </RelativeLayout>
+ <RelativeLayout
+ android:id="@+id/log_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ tools:visibility="visible"
+ >
+ <androidx.appcompat.widget.AppCompatTextView
+ android:id="@+id/connection_detail_logs_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/log_fragment_title"
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Medium"
+ android:layout_alignParentTop="true"
+ android:paddingStart="4dp"
+ android:paddingLeft="4dp"
+ android:paddingEnd="4dp"
+ android:paddingRight="4dp"
+ android:paddingTop="@dimen/stdpadding"
+ android:paddingBottom="@dimen/stdpadding"
+ />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/connection_detail_logs"
+ android:layout_below="@+id/connection_detail_logs_title"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:listitem="@layout/v_log_item"
+ android:isScrollContainer="false"
+ />
+
+ <ImageView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/connection_detail_logs"
+ android:layout_alignTop="@id/connection_detail_logs"
+ android:src="@drawable/v_vertical_gradient"
+ android:layout_marginTop="200dp"
+ android:importantForAccessibility="no"
+ />
+ </RelativeLayout>
+
</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/v_log_item.xml b/app/src/main/res/layout/v_log_item.xml
new file mode 100644
index 00000000..713c3b1f
--- /dev/null
+++ b/app/src/main/res/layout/v_log_item.xml
@@ -0,0 +1,29 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:orientation="vertical"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ >
+ <TextView
+ android:id="@android:id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:layout_gravity="center_vertical"
+ tools:text="test"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+ android:background="?android:attr/activatedBackgroundIndicator"
+ android:paddingTop="3dp"
+ android:paddingBottom="3dp"
+ android:minHeight="10dp"
+ />
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/black800_high_transparent"
+ />
+
+</LinearLayout>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e883b974..22696c12 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -7,6 +7,7 @@
<string name="switch_provider_menu_option">Switch provider</string>
<string name="info">info</string>
<string name="show_connection_details">Show connection details</string>
+ <string name="connection_details">Connection details</string>
<string name="routes_info">Routes: %s</string>
<string name="routes_info6">IPv6 routes: %s</string>
<string name="error_empty_username">The username must not be empty.</string>
@@ -155,4 +156,30 @@
<string name="gateway_selection_automatic">Automatic</string>
<string name="gateway_selection_current_location">Your traffic is currently routed through: </string>
+ <string name="tor_starting">Starting bridges for censorship circumvention…</string>
+ <string name="tor_stopping">Stopping bridges.</string>
+ <string name="tor_started">Using bridges for censorship circumvention.</string>
+ <string name="log_conn_done_pt">Connected to pluggable transport</string>
+ <string name="log_conn_pt">Connecting to pluggable transport</string>
+ <string name="log_conn_done">Connected to a relay</string>
+ <string name="log_handshake">Handshaking with a relay</string>
+ <string name="log_handshake_done">Handshake with a relay done</string>
+ <string name="log_onehop_create">Establishing an encrypted directory connection</string>
+ <string name="log_requesting_status">Asking for networkstatus consensus</string>
+ <string name="log_loading_status">Loading networkstatus consensus</string>
+ <string name="log_loading_keys">Loading authority key certs</string>
+ <string name="log_requesting_desccriptors">Asking for relay descriptors</string>
+ <string name="log_loading_descriptors">Loading relay descriptors</string>
+ <string name="log_enough_dirinfo">Loaded enough directory info to build circuits</string>
+ <string name="log_ap_handshake_done">Handshake finished with a relay to build circuits</string>
+ <string name="log_circuit_create">Establishing a Tor circuit</string>
+ <string name="log_done">Running</string>
+ <string name="channel_name_tor_service">%s Bridges Service</string>
+ <string name="channel_description_tor_service">Informs about usage of bridges while configuring %s.</string>
+ <string name="error_tor_timeout">Starting bridges failed. Do you want to retry or continue with an unobfuscated secure connection to configure %s?</string>
+ <string name="retry_unobfuscated">Retry unobfuscated</string>
+ <string name="hide_connection_details">Hide connection details</string>
+ <string name="error_network_connection">%s has no internet connection. Please check your WiFi and cellular data settings.</string>
+
+
</resources>
diff --git a/app/src/production/java/se/leap/bitmaskclient/providersetup/ProviderApiManager.java b/app/src/production/java/se/leap/bitmaskclient/providersetup/ProviderApiManager.java
index 70652365..5416b1f8 100644
--- a/app/src/production/java/se/leap/bitmaskclient/providersetup/ProviderApiManager.java
+++ b/app/src/production/java/se/leap/bitmaskclient/providersetup/ProviderApiManager.java
@@ -28,14 +28,18 @@ import org.json.JSONObject;
import java.io.IOException;
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.eip.EipStatus;
import se.leap.bitmaskclient.providersetup.connectivity.OkHttpClientGenerator;
+import se.leap.bitmaskclient.tor.TorStatusObservable;
import static android.text.TextUtils.isEmpty;
import static se.leap.bitmaskclient.BuildConfig.DEBUG_MODE;
@@ -52,6 +56,8 @@ import static se.leap.bitmaskclient.base.utils.ConfigHelper.getProviderFormatted
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.tor.TorStatusObservable.TorStatus.OFF;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.getProxyPort;
/**
* Implements the logic of the provider api http requests. The methods of this class need to be called from
@@ -221,7 +227,7 @@ public class ProviderApiManager extends ProviderApiManagerBase {
/**
* 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 is not yet active. The latter condition is needed in order to guarantee that the geoip service sees
+ * 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
@@ -231,7 +237,7 @@ public class ProviderApiManager extends ProviderApiManagerBase {
protected Bundle getGeoIPJson(Provider provider) {
Bundle result = new Bundle();
- if (!provider.shouldUpdateGeoIpJson() || provider.getGeoipUrl().isDefault() || VpnStatus.isVPNActive()) {
+ if (!provider.shouldUpdateGeoIpJson() || provider.getGeoipUrl().isDefault() || VpnStatus.isVPNActive() || TorStatusObservable.getStatus() != OFF) {
result.putBoolean(BROADCAST_RESULT_KEY, false);
return result;
}
@@ -239,7 +245,7 @@ public class ProviderApiManager extends ProviderApiManagerBase {
try {
URL geoIpUrl = provider.getGeoipUrl().getUrl();
- String geoipJsonString = downloadFromUrlWithProviderCA(geoIpUrl.toString(), provider);
+ String geoipJsonString = downloadFromUrlWithProviderCA(geoIpUrl.toString(), provider, false);
if (DEBUG_MODE) {
VpnStatus.logDebug("[API] MENSHEN JSON: " + geoipJsonString);
}
@@ -285,15 +291,20 @@ public class ProviderApiManager extends ProviderApiManagerBase {
return result;
}
- /**
- * Tries to download the contents of the provided url using commercially validated CA certificate from chosen provider.
- *
- */
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);
+ OkHttpClient okHttpClient = clientGenerator.initCommercialCAHttpClient(errorJson, getProxyPort());
if (okHttpClient == null) {
return errorJson.toString();
}
@@ -314,6 +325,18 @@ public class ProviderApiManager extends ProviderApiManagerBase {
}
}
+ 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;
}
@@ -330,9 +353,13 @@ public class ProviderApiManager extends ProviderApiManagerBase {
}
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(), errorJson);
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), getProxyPort(), errorJson);
if (okHttpClient == null) {
return errorJson.toString();
}
@@ -340,6 +367,19 @@ public class ProviderApiManager extends ProviderApiManagerBase {
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;
}
@@ -354,7 +394,7 @@ public class ProviderApiManager extends ProviderApiManagerBase {
JSONObject initError = new JSONObject();
String responseString;
- OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(caCert, initError);
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(caCert, getProxyPort(), initError);
if (okHttpClient == null) {
return initError.toString();
}
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 81fcd7d5..08591b6d 100644
--- a/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
@@ -81,7 +81,7 @@ public class GatewaysManagerTest {
@Test
public void testGatewayManagerFromCurrentProvider_noProvider_noGateways() {
- MockHelper.mockProviderObserver(null);
+ MockHelper.mockProviderObservable(null);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
assertEquals(0, gatewaysManager.size());
}
@@ -89,7 +89,7 @@ public class GatewaysManagerTest {
@Test
public void testGatewayManagerFromCurrentProvider_misconfiguredProvider_noGateways() throws IOException, NullPointerException {
Provider provider = getProvider(null, null, null, null, null, null, "ptdemo_misconfigured_gateway.json", null);
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
assertEquals(0, gatewaysManager.size());
}
@@ -97,7 +97,7 @@ public class GatewaysManagerTest {
@Test
public void testGatewayManagerFromCurrentProvider_threeGateways() {
Provider provider = getProvider(null, null, null, null,null, null, "ptdemo_three_mixed_gateways.json", null);
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
assertEquals(3, gatewaysManager.size());
}
@@ -107,7 +107,7 @@ public class GatewaysManagerTest {
Provider provider = getProvider(null, null, null, null, null, null, "ptdemo_three_mixed_gateways.json", null);
JSONObject eipServiceJson = provider.getEipServiceJson();
JSONObject gateway1 = eipServiceJson.getJSONArray(GATEWAYS).getJSONObject(0);
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, gateway1, 3);
@@ -122,7 +122,7 @@ public class GatewaysManagerTest {
Provider provider = getProvider(null, null, null, null, null, null, "ptdemo_three_mixed_gateways.json", null);
JSONObject eipServiceJson = provider.getEipServiceJson();
JSONObject gateway1 = eipServiceJson.getJSONArray(GATEWAYS).getJSONObject(0);
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, gateway1, 3);
@@ -137,7 +137,7 @@ public class GatewaysManagerTest {
Provider provider = getProvider(null, null, null, null, null, null, "ptdemo_three_mixed_gateways.json", "ptdemo_three_mixed_gateways.geoip.json");
JSONObject eipServiceJson = provider.getEipServiceJson();
JSONObject gateway1 = eipServiceJson.getJSONArray(GATEWAYS).getJSONObject(0);
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, gateway1, 3);
@@ -152,7 +152,7 @@ public class GatewaysManagerTest {
Provider provider = getProvider(null, null, null, null, null, null, "ptdemo_three_mixed_gateways.json", "ptdemo_three_mixed_gateways.geoip.json");
JSONObject eipServiceJson = provider.getEipServiceJson();
JSONObject gateway1 = eipServiceJson.getJSONArray(GATEWAYS).getJSONObject(0);
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, gateway1, 3);
@@ -167,7 +167,7 @@ public class GatewaysManagerTest {
Provider provider = getProvider(null, null, null, null, null, null, "ptdemo_three_mixed_gateways.json", null);
JSONObject eipServiceJson = provider.getEipServiceJson();
JSONObject gateway1 = eipServiceJson.getJSONArray(GATEWAYS).getJSONObject(0);
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, gateway1, 3);
@@ -182,7 +182,7 @@ public class GatewaysManagerTest {
Provider provider = getProvider(null, null, null, null, null, null, "ptdemo_three_mixed_gateways.json", null);
JSONObject eipServiceJson = provider.getEipServiceJson();
JSONObject gateway1 = eipServiceJson.getJSONArray(GATEWAYS).getJSONObject(1);
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
VpnConfigGenerator configGenerator = new VpnConfigGenerator(provider.getDefinition(), secrets, gateway1, 3);
@@ -196,9 +196,9 @@ public class GatewaysManagerTest {
public void TestSelectN_selectFirstObfs4Connection_returnThirdGateway() {
Provider provider = getProvider(null, null, null, null, null, null, "ptdemo_two_openvpn_one_pt_gateways.json", null);
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
mockStatic(PreferenceHelper.class);
- when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(true);
+ when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(true);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
assertEquals("37.12.247.10", gatewaysManager.select(0).getRemoteIP());
@@ -208,10 +208,10 @@ public class GatewaysManagerTest {
public void testSelectN_selectFromPresortedGateways_returnsGatewaysInPresortedOrder() {
Provider provider = getProvider(null, null, null, null, null, null, "ptdemo_three_mixed_gateways.json", "ptdemo_three_mixed_gateways.geoip.json");
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
//use openvpn, not pluggable transports
mockStatic(PreferenceHelper.class);
- when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(false);
+ when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(false);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
assertEquals("manila.bitmask.net", gatewaysManager.select(0).getHost());
@@ -223,10 +223,10 @@ public class GatewaysManagerTest {
public void testSelectN_selectObfs4FromPresortedGateways_returnsObfs4GatewaysInPresortedOrder() {
Provider provider = getProvider(null, null, null, null, null, null, "ptdemo_three_mixed_gateways.json", "ptdemo_three_mixed_gateways.geoip.json");
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
//use openvpn, not pluggable transports
mockStatic(PreferenceHelper.class);
- when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(true);
+ when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(true);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
assertEquals("moscow.bitmask.net", gatewaysManager.select(0).getHost());
@@ -239,10 +239,10 @@ public class GatewaysManagerTest {
public void testSelectN_selectFromCity_returnsGatewaysInPresortedOrder() {
Provider provider = getProvider(null, null, null, null, null, null, "v4/riseup_eipservice_for_geoip_v4.json", "v4/riseup_geoip_v4.json");
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
//use openvpn, not pluggable transports
mockStatic(PreferenceHelper.class);
- when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(false);
+ when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(false);
when(PreferenceHelper.getPreferredCity(any(Context.class))).thenReturn("Paris");
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
@@ -255,10 +255,10 @@ public class GatewaysManagerTest {
public void testSelectN_selectFromCityWithGeoIpServiceV1_returnsGatewaysInPresortedOrder() {
Provider provider = getProvider(null, null, null, null, null, null, "v4/riseup_eipservice_for_geoip_v4.json", "v4/riseup_geoip_v1.json");
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
//use openvpn, not pluggable transports
mockStatic(PreferenceHelper.class);
- when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(false);
+ when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(false);
when(PreferenceHelper.getPreferredCity(any(Context.class))).thenReturn("Paris");
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
@@ -272,10 +272,10 @@ public class GatewaysManagerTest {
Provider provider = getProvider(null, null, null, null, null, null, "v4/riseup_eipservice_for_geoip_v4.json", null);
provider.setGeoIpJson(new JSONObject());
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
//use openvpn, not pluggable transports
mockStatic(PreferenceHelper.class);
- when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(false);
+ when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(false);
when(PreferenceHelper.getPreferredCity(any(Context.class))).thenReturn("Paris");
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
@@ -289,10 +289,10 @@ public class GatewaysManagerTest {
public void testSelectN_selectNAndCity_returnsGatewaysInPresortedOrder() {
Provider provider = getProvider(null, null, null, null, null, null, "v4/riseup_eipservice_for_geoip_v4.json", "v4/riseup_geoip_v4.json");
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
//use openvpn, not pluggable transports
mockStatic(PreferenceHelper.class);
- when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(false);
+ when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(false);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
assertEquals("mouette.riseup.net", gatewaysManager.select(0, "Paris").getHost());
@@ -304,10 +304,10 @@ public class GatewaysManagerTest {
public void testSelectN_selectNAndCityWithGeoIpServiceV1_returnsGatewaysInPresortedOrder() {
Provider provider = getProvider(null, null, null, null, null, null, "v4/riseup_eipservice_for_geoip_v4.json", "v4/riseup_geoip_v1.json");
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
//use openvpn, not pluggable transports
mockStatic(PreferenceHelper.class);
- when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(false);
+ when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(false);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
assertEquals("mouette.riseup.net", gatewaysManager.select(0, "Paris").getHost());
@@ -320,10 +320,10 @@ public class GatewaysManagerTest {
Provider provider = getProvider(null, null, null, null, null, null, "v4/riseup_eipservice_for_geoip_v4.json", null);
provider.setGeoIpJson(new JSONObject());
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
//use openvpn, not pluggable transports
mockStatic(PreferenceHelper.class);
- when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(false);
+ when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(false);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
assertEquals("Paris", gatewaysManager.select(0, "Paris").getName());
@@ -337,10 +337,10 @@ public class GatewaysManagerTest {
Provider provider = getProvider(null, null, null, null, null, null, "v4/riseup_eipservice_for_geoip_v4.json", "v4/riseup_geoip_v4.json");
provider.setGeoIpJson(new JSONObject());
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
//use openvpn, not pluggable transports
mockStatic(PreferenceHelper.class);
- when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(false);
+ when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(false);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
assertNull(gatewaysManager.select(0, "Stockholm"));
}
@@ -349,9 +349,9 @@ public class GatewaysManagerTest {
public void testGetLocations_openvpn() {
Provider provider = getProvider(null, null, null, null, null, null, "v4/riseup_eipservice_for_geoip_v4.json", "v4/riseup_geoip_v4.json");
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
mockStatic(PreferenceHelper.class);
- when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(false);
+ when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(false);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
List<Location> locations = gatewaysManager.getGatewayLocations();
@@ -370,9 +370,9 @@ public class GatewaysManagerTest {
public void testGetLocations_obfs4() {
Provider provider = getProvider(null, null, null, null, null, null, "v4/riseup_eipservice_for_geoip_v4.json", "v4/riseup_geoip_v4.json");
- MockHelper.mockProviderObserver(provider);
+ MockHelper.mockProviderObservable(provider);
mockStatic(PreferenceHelper.class);
- when(PreferenceHelper.getUsePluggableTransports(any(Context.class))).thenReturn(true);
+ when(PreferenceHelper.getUseBridges(any(Context.class))).thenReturn(true);
GatewaysManager gatewaysManager = new GatewaysManager(mockContext);
List<Location> locations = gatewaysManager.getGatewayLocations();
diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java
index dbcb86b8..2b1dc2ef 100644
--- a/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java
@@ -24,6 +24,8 @@ import android.content.res.Resources;
import android.os.Bundle;
import android.text.TextUtils;
+import androidx.annotation.Nullable;
+
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Before;
@@ -38,30 +40,39 @@ import org.powermock.modules.junit4.PowerMockRunner;
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.ConfigHelper;
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
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.testutils.MockSharedPreferences;
-import se.leap.bitmaskclient.base.utils.ConfigHelper;
-import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+import se.leap.bitmaskclient.tor.TorStatusObservable;
+import static org.junit.Assert.assertEquals;
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.USE_BRIDGES;
+import static se.leap.bitmaskclient.base.models.Constants.USE_TOR;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_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.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_TIMEOUT;
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;
+import static se.leap.bitmaskclient.testutils.BackendMockResponses.BackendMockProvider.TestBackendErrorCase.ERROR_DNS_RESUOLUTION_TOR_FALLBACK;
import static se.leap.bitmaskclient.testutils.BackendMockResponses.BackendMockProvider.TestBackendErrorCase.ERROR_GEOIP_SERVICE_IS_DOWN;
+import static se.leap.bitmaskclient.testutils.BackendMockResponses.BackendMockProvider.TestBackendErrorCase.ERROR_GEOIP_SERVICE_IS_DOWN_TOR_FALLBACK;
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.mockBundle;
@@ -74,6 +85,7 @@ import static se.leap.bitmaskclient.testutils.MockHelper.mockProviderApiConnecto
import static se.leap.bitmaskclient.testutils.MockHelper.mockResources;
import static se.leap.bitmaskclient.testutils.MockHelper.mockResultReceiver;
import static se.leap.bitmaskclient.testutils.MockHelper.mockTextUtils;
+import static se.leap.bitmaskclient.testutils.MockHelper.mockTorStatusObservable;
import static se.leap.bitmaskclient.testutils.TestSetupHelper.getConfiguredProvider;
import static se.leap.bitmaskclient.testutils.TestSetupHelper.getConfiguredProviderAPIv4;
import static se.leap.bitmaskclient.testutils.TestSetupHelper.getInputAsString;
@@ -85,7 +97,7 @@ import static se.leap.bitmaskclient.testutils.TestSetupHelper.getProvider;
*/
@RunWith(PowerMockRunner.class)
-@PrepareForTest({ProviderApiManager.class, TextUtils.class, ConfigHelper.class, ProviderApiConnector.class, PreferenceHelper.class})
+@PrepareForTest({ProviderApiManager.class, TextUtils.class, ConfigHelper.class, ProviderApiConnector.class, PreferenceHelper.class, TorStatusObservable.class})
public class ProviderApiManagerTest {
private SharedPreferences mockPreferences;
@@ -96,17 +108,47 @@ public class ProviderApiManagerTest {
private ProviderApiManager providerApiManager;
- class TestProviderApiServiceCallback implements ProviderApiManagerBase.ProviderApiServiceCallback {
-
- //Intent expectedIntent;
- TestProviderApiServiceCallback(/*Intent expectedIntent*/) {
- //this.expectedIntent = expectedIntent;
+ static class TestProviderApiServiceCallback implements ProviderApiManagerBase.ProviderApiServiceCallback {
+ Throwable startTorServiceException;
+ boolean hasNetworkConnection;
+ TestProviderApiServiceCallback() {
+ new TestProviderApiServiceCallback(null, true);
+ }
+ TestProviderApiServiceCallback(@Nullable Throwable startTorServiceException, boolean hasNetworkConnection) {
+ this.startTorServiceException = startTorServiceException;
}
@Override
public void broadcastEvent(Intent intent) {
- //assertEquals("expected intent: ", expectedIntent, 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;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void stopTorService() {
+ }
+
+ @Override
+ public int getTorHttpTunnelPort() {
+ return 0;
+ }
+
+ @Override
+ public boolean hasNetworkConnection() {
+ return hasNetworkConnection;
+ }
+
}
@Before
@@ -482,6 +524,37 @@ public class ProviderApiManagerTest {
Provider provider = getConfiguredProvider();
mockFingerprintForCertificate("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
mockProviderApiConnector(ERROR_GEOIP_SERVICE_IS_DOWN);
+ mockPreferences.edit().putBoolean(USE_BRIDGES, false).putBoolean(USE_TOR, false).commit();
+ providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+
+ Bundle expectedResult = mockBundle();
+ expectedResult.putBoolean(EIP_ACTION_START, true);
+ expectedResult.putBoolean(BROADCAST_RESULT_KEY, false);
+ expectedResult.putParcelable(PROVIDER_KEY, provider);
+
+ Intent providerApiCommand = mockIntent();
+
+ providerApiCommand.setAction(ProviderAPI.DOWNLOAD_GEOIP_JSON);
+ Bundle extrasBundle = mockBundle();
+ 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);
+
+ }
+
+ @Test
+ public void test_handleIntentGetGeoip_serviceDown_torNotStarted() throws IOException, NoSuchAlgorithmException, CertificateEncodingException, JSONException, TimeoutException, InterruptedException {
+ if ("insecure".equals(BuildConfig.FLAVOR_implementation)) {
+ return;
+ }
+
+ mockTorStatusObservable(null);
+ Provider provider = getConfiguredProvider();
+ mockFingerprintForCertificate("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+ mockProviderApiConnector(ERROR_GEOIP_SERVICE_IS_DOWN_TOR_FALLBACK);
providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
Bundle expectedResult = mockBundle();
@@ -499,6 +572,8 @@ public class ProviderApiManagerTest {
providerApiCommand.putExtra(PARAMETERS, extrasBundle);
providerApiManager.handleIntent(providerApiCommand);
+ // also assert that Tor was not allowed to start
+ assertEquals(-1, TorStatusObservable.getProxyPort());
}
@@ -582,4 +657,148 @@ public class ProviderApiManagerTest {
providerApiManager.handleIntent(providerApiCommand);
}
+ @Test
+ public void test_handleIntentSetupProvider_TorFallback_SecondTryHappyPath() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, TimeoutException, InterruptedException {
+ Provider provider = getConfiguredProviderAPIv4();
+
+ mockFingerprintForCertificate(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+ mockProviderApiConnector(ERROR_DNS_RESUOLUTION_TOR_FALLBACK);
+ providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+
+ Intent providerApiCommand = mockIntent();
+ providerApiCommand.putExtra(PROVIDER_KEY, provider);
+ providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
+ providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK));
+
+ mockTorStatusObservable(null);
+
+ providerApiManager.handleIntent(providerApiCommand);
+ assertEquals(8118, TorStatusObservable.getProxyPort());
+ }
+
+ @Test
+ public void test_handleIntentSetupProvider_TorFallbackStartServiceException_SecondTryFailed() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, TimeoutException, InterruptedException {
+ Provider provider = getConfiguredProviderAPIv4();
+
+ mockFingerprintForCertificate(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+ mockProviderApiConnector(ERROR_DNS_RESUOLUTION_TOR_FALLBACK);
+ providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback(new IllegalStateException("Tor service start not failed."), true));
+
+ Intent providerApiCommand = mockIntent();
+ providerApiCommand.putExtra(PROVIDER_KEY, provider);
+ providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
+ providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK));
+
+ mockTorStatusObservable(null);
+
+ providerApiManager.handleIntent(providerApiCommand);
+ assertEquals(-1, TorStatusObservable.getProxyPort());
+ }
+
+ @Test
+ public void test_handleIntentSetupProvider_TorFallbackTimeoutException_SecondTryFailed() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, TimeoutException, InterruptedException {
+ Provider provider = getConfiguredProviderAPIv4();
+
+ mockFingerprintForCertificate(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+ mockProviderApiConnector(ERROR_DNS_RESUOLUTION_TOR_FALLBACK);
+ providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+
+ Intent providerApiCommand = mockIntent();
+ providerApiCommand.putExtra(PROVIDER_KEY, provider);
+ providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
+ providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK));
+
+ mockTorStatusObservable(new TimeoutException("Tor took too long to start."));
+
+ providerApiManager.handleIntent(providerApiCommand);
+ assertEquals(-1, TorStatusObservable.getProxyPort());
+ }
+
+ @Test
+ public void test_handleIntentSetupProvider_TorBridgesPreferenceEnabled_Success() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, TimeoutException, InterruptedException {
+ Provider provider = getConfiguredProviderAPIv4();
+
+ mockFingerprintForCertificate(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+ mockProviderApiConnector(NO_ERROR_API_V4);
+
+ mockPreferences.edit().putBoolean(USE_BRIDGES, true).putBoolean(USE_TOR, true).commit();
+ providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+
+ Intent providerApiCommand = mockIntent();
+ providerApiCommand.putExtra(PROVIDER_KEY, provider);
+ providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
+ providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK));
+
+ mockTorStatusObservable(null);
+
+ providerApiManager.handleIntent(providerApiCommand);
+ assertEquals(8118, TorStatusObservable.getProxyPort());
+ }
+
+ @Test
+ public void test_handleIntentSetupProvider_TorBridgesDisabled_TorNotStarted() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, TimeoutException, InterruptedException {
+ Provider provider = getConfiguredProviderAPIv4();
+
+ mockFingerprintForCertificate(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+ mockProviderApiConnector(NO_ERROR_API_V4);
+
+ mockPreferences.edit().putBoolean(USE_BRIDGES, false).putBoolean(USE_TOR, false).commit();
+ providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+
+ Intent providerApiCommand = mockIntent();
+ providerApiCommand.putExtra(PROVIDER_KEY, provider);
+ providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
+ providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK));
+
+ mockTorStatusObservable(new TimeoutException("This timeout exception is never thrown"));
+
+ providerApiManager.handleIntent(providerApiCommand);
+ assertEquals(-1, TorStatusObservable.getProxyPort());
+ }
+
+ @Test
+ public void test_handleIntentSetupProvider_TorBridgesPreferencesEnabledTimeout_TimeoutError() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, TimeoutException, InterruptedException {
+ Provider provider = getConfiguredProviderAPIv4();
+
+ mockPreferences.edit().putBoolean(USE_BRIDGES, true).putBoolean(USE_TOR, true).commit();
+ providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+
+ Bundle expectedResult = mockBundle();
+ 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 = mockIntent();
+ providerApiCommand.putExtra(PROVIDER_KEY, provider);
+ providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
+ providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(TOR_TIMEOUT, expectedResult));
+
+ mockTorStatusObservable(new TimeoutException("Tor took too long to start."));
+
+ providerApiManager.handleIntent(providerApiCommand);
+ assertEquals(-1, TorStatusObservable.getProxyPort());
+ }
+
+ @Test
+ public void test_handleIntentSetupProvider_noNetwork_NetworkError() throws IOException, CertificateEncodingException, NoSuchAlgorithmException, JSONException {
+ Provider provider = getConfiguredProvider();
+
+ mockFingerprintForCertificate("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+ mockProviderApiConnector(NO_ERROR);
+ providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback(null, false));
+ Bundle expectedResult = mockBundle();
+
+ 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 = mockIntent();
+
+ providerApiCommand.setAction(ProviderAPI.SET_UP_PROVIDER);
+ providerApiCommand.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(MISSING_NETWORK_CONNECTION, expectedResult));
+ providerApiCommand.putExtra(PROVIDER_KEY, provider);
+
+ providerApiManager.handleIntent(providerApiCommand);
+ }
+
}
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BackendMockProvider.java b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BackendMockProvider.java
index d76e4029..280aa5a1 100644
--- a/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BackendMockProvider.java
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/BackendMockProvider.java
@@ -33,6 +33,7 @@ public class BackendMockProvider {
ERROR_CASE_MICONFIGURED_PROVIDER,
ERROR_CASE_FETCH_EIP_SERVICE_CERTIFICATE_INVALID,
ERROR_GEOIP_SERVICE_IS_DOWN,
+ ERROR_GEOIP_SERVICE_IS_DOWN_TOR_FALLBACK,
ERROR_NO_RESPONSE_BODY, // => NullPointerException
ERROR_DNS_RESOLUTION_ERROR, // => UnkownHostException
ERROR_SOCKET_TIMEOUT, // => SocketTimeoutException
@@ -46,7 +47,8 @@ public class BackendMockProvider {
ERROR_INVALID_SESSION_TOKEN,
ERROR_NO_CONNECTION,
ERROR_WRONG_SRP_CREDENTIALS,
- NO_ERROR_API_V4
+ NO_ERROR_API_V4,
+ ERROR_DNS_RESUOLUTION_TOR_FALLBACK
}
@@ -71,6 +73,11 @@ public class BackendMockProvider {
case ERROR_GEOIP_SERVICE_IS_DOWN:
new GeoIpServiceIsDownBackendResponse();
break;
+ case ERROR_GEOIP_SERVICE_IS_DOWN_TOR_FALLBACK:
+ new GeoIpServiceNotReachableTorFallbackBackendResponse();
+ case ERROR_DNS_RESUOLUTION_TOR_FALLBACK:
+ new TorFallbackBackendResponse();
+ break;
case ERROR_NO_RESPONSE_BODY:
break;
case ERROR_DNS_RESOLUTION_ERROR:
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/GeoIpServiceNotReachableTorFallbackBackendResponse.java b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/GeoIpServiceNotReachableTorFallbackBackendResponse.java
new file mode 100644
index 00000000..02aa31fa
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/GeoIpServiceNotReachableTorFallbackBackendResponse.java
@@ -0,0 +1,89 @@
+/**
+ * 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.testutils.BackendMockResponses;
+
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.IOException;
+import java.net.ConnectException;
+
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.getInputAsString;
+
+/**
+ * Created by cyberta on 10.01.18.
+ */
+
+public class GeoIpServiceNotReachableTorFallbackBackendResponse extends BaseBackendResponse {
+ public GeoIpServiceNotReachableTorFallbackBackendResponse() throws IOException {
+ super();
+ }
+ int requestAttempt = 0;
+
+ @Override
+ public Answer<String> getAnswerForRequestStringFromServer() {
+ return new Answer<String>() {
+ @Override
+ public String answer(InvocationOnMock invocation) throws Throwable {
+ String url = (String) invocation.getArguments()[0];
+
+ if (url.contains("/provider.json")) {
+ //download provider json
+ return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"));
+ } else if (url.contains("/ca.crt")) {
+ //download provider ca cert
+ return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem"));
+ } else if (url.contains("config/eip-service.json")) {
+ // download provider service json containing gateways, locations and openvpn settings
+ return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.service.json"));
+ } else if (url.contains(":9001/json")) {
+ if (requestAttempt == 0) {
+ // download geoip json, containing a sorted list of gateways
+ requestAttempt++;
+ throw new ConnectException("Failed to connect to api.black.riseup.net/198.252.153.107:9001");
+ } else {
+ // assumtion: 2. connection attempt has been made with proxy on, which is not allowed
+ // this branch should never be called otherwise you have found a bug
+ return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.geoip.json"));
+ }
+ }
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public Answer<Boolean> getAnswerForCanConnect() {
+ return new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ return true;
+ }
+ };
+ }
+
+ @Override
+ public Answer<Boolean> getAnswerForDelete() {
+ return new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ return true;
+ }
+ };
+ }
+
+}
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/TorFallbackBackendResponse.java b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/TorFallbackBackendResponse.java
new file mode 100644
index 00000000..dc12ae89
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/BackendMockResponses/TorFallbackBackendResponse.java
@@ -0,0 +1,86 @@
+package se.leap.bitmaskclient.testutils.BackendMockResponses;
+
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.IOException;
+import java.net.UnknownHostException;
+
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.getInputAsString;
+
+public class TorFallbackBackendResponse extends BaseBackendResponse {
+ public TorFallbackBackendResponse() throws IOException {
+ super();
+ }
+ int requestAttempt = 0;
+
+ @Override
+ public Answer<String> getAnswerForRequestStringFromServer() {
+ return new Answer<String>() {
+ @Override
+ public String answer(InvocationOnMock invocation) throws Throwable {
+ String url = (String) invocation.getArguments()[0];
+
+ if (url.contains("/provider.json")) {
+ if (requestAttempt == 0) {
+ requestAttempt++;
+ throw new UnknownHostException();
+ }
+ //download provider json
+ return getInputAsString(getClass().getClassLoader().getResourceAsStream("v4/riseup.net.json"));
+ } else if (url.contains("/ca.crt")) {
+ if (requestAttempt == 0) {
+ requestAttempt++;
+ throw new UnknownHostException("DNS blocked by censor ;)");
+ }
+ //download provider ca cert
+ return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem"));
+ } else if (url.contains("config/eip-service.json")) {
+ if (requestAttempt == 0) {
+ requestAttempt++;
+ throw new UnknownHostException("DNS blocked by censor ;)");
+ }
+ // download provider service json containing gateways, locations and openvpn settings
+ return getInputAsString(getClass().getClassLoader().getResourceAsStream("v4/riseup.service.json"));
+ } else if (url.contains(":9001/json")) {
+ if (requestAttempt == 0) {
+ requestAttempt++;
+ throw new UnknownHostException("DNS blocked by censor ;)");
+ }
+ // download geoip json, containing a sorted list of gateways
+ return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.geoip.json"));
+ }
+
+ return null;
+ }
+ };
+ }
+
+ @Override
+ public Answer<Boolean> getAnswerForCanConnect() {
+ return new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ if (requestAttempt == 0) {
+ requestAttempt++;
+ throw new UnknownHostException("DNS blocked by censor ;)");
+ }
+ return true;
+ }
+ };
+ }
+
+ @Override
+ public Answer<Boolean> getAnswerForDelete() {
+ return new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ if (requestAttempt == 0) {
+ requestAttempt++;
+ throw new UnknownHostException("DNS blocked by censor ;)");
+ }
+ return true;
+ }
+ };
+ }
+}
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 5a341cd1..0086e4c2 100644
--- a/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/MockHelper.java
@@ -9,9 +9,11 @@ import android.content.res.Resources;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.ResultReceiver;
-import androidx.annotation.NonNull;
import android.text.TextUtils;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import org.json.JSONException;
import org.json.JSONObject;
import org.mockito.invocation.InvocationOnMock;
@@ -32,18 +34,22 @@ 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.providersetup.connectivity.OkHttpClientGenerator;
+import se.leap.bitmaskclient.R;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.base.models.ProviderObservable;
-import se.leap.bitmaskclient.R;
-import se.leap.bitmaskclient.testutils.BackendMockResponses.BackendMockProvider;
-import se.leap.bitmaskclient.testutils.matchers.BundleMatcher;
import se.leap.bitmaskclient.base.utils.ConfigHelper;
import se.leap.bitmaskclient.base.utils.FileHelper;
import se.leap.bitmaskclient.base.utils.InputStreamHelper;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
+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;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
@@ -202,6 +208,18 @@ public class MockHelper {
}
});
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ String key = (String) invocation.getArguments()[0];
+ fakeBooleanBundle.remove(key);
+ fakeIntBundle.remove(key);
+ fakeParcelableBundle.remove(key);
+ fakeStringBundle.remove(key);
+ return null;
+ }
+ }).when(bundle).remove(anyString());
+
return bundle;
}
@@ -319,6 +337,20 @@ public class MockHelper {
});
}
+ public static ResultReceiver mockResultReceiver(final int expectedResultCode) {
+ ResultReceiver resultReceiver = mock(ResultReceiver.class);
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ Object[] arguments = invocation.getArguments();
+ int resultCode = (int) arguments[0];
+ assertEquals("expected resultCode: ", expectedResultCode, resultCode);
+ return null;
+ }
+ }).when(resultReceiver).send(anyInt(), any(Bundle.class));
+ return resultReceiver;
+ }
+
public static ResultReceiver mockResultReceiver(final int expectedResultCode, final Bundle expectedBundle) {
ResultReceiver resultReceiver = mock(ResultReceiver.class);
@@ -423,7 +455,46 @@ public class MockHelper {
});
}
- public static void mockProviderObserver(Provider provider) {
+ public static void mockTorStatusObservable(@Nullable Throwable exception) throws TimeoutException, InterruptedException {
+ TorStatusObservable observable = TorStatusObservable.getInstance();
+ mockStatic(TorStatusObservable.class);
+ when(TorStatusObservable.getInstance()).thenAnswer((Answer<TorStatusObservable>) invocation -> observable);
+
+ when(TorStatusObservable.getBootstrapProgress()).thenReturn(0);
+ when(TorStatusObservable.getLastLogs()).thenReturn(new Vector<>());
+ when(TorStatusObservable.getLastTorLog()).thenReturn("");
+ when(TorStatusObservable.getLastSnowflakeLog()).thenReturn("");
+ AtomicBoolean waitUntilSuccess = new AtomicBoolean(false);
+ when(TorStatusObservable.getProxyPort()).thenAnswer((Answer<Integer>) invocation -> {
+ if (waitUntilSuccess.get()) {
+ return 8118;
+ }
+ return -1;
+ });
+ when(TorStatusObservable.getStatus()).thenAnswer((Answer<TorStatusObservable.TorStatus>) invocation -> {
+ if (waitUntilSuccess.get()) {
+ return TorStatusObservable.TorStatus.ON;
+ }
+ return TorStatusObservable.TorStatus.OFF;
+ });
+ when(TorStatusObservable.getSnowflakeStatus()).thenAnswer((Answer<TorStatusObservable.SnowflakeStatus>) invocation -> {
+ if (waitUntilSuccess.get()) {
+ return TorStatusObservable.SnowflakeStatus.ON;
+ }
+ return TorStatusObservable.SnowflakeStatus.OFF;
+ });
+
+ if (exception != null) {
+ when(TorStatusObservable.waitUntil(any(TorStatusObservable.StatusCondition.class), anyInt())).thenThrow(exception);
+ } else {
+ when(TorStatusObservable.waitUntil(any(TorStatusObservable.StatusCondition.class), anyInt())).thenAnswer((Answer<Boolean>) invocation -> {
+ waitUntilSuccess.set(true);
+ return true;
+ });
+ }
+ }
+
+ public static void mockProviderObservable(Provider provider) {
ProviderObservable observable = ProviderObservable.getInstance();
observable.updateProvider(provider);
mockStatic(ProviderObservable.class);
@@ -445,8 +516,8 @@ public class MockHelper {
public static OkHttpClientGenerator mockClientGenerator(boolean resolveDNS) throws UnknownHostException {
OkHttpClientGenerator mockClientGenerator = mock(OkHttpClientGenerator.class);
OkHttpClient mockedOkHttpClient = mock(OkHttpClient.class, RETURNS_DEEP_STUBS);
- when(mockClientGenerator.initCommercialCAHttpClient(any(JSONObject.class))).thenReturn(mockedOkHttpClient);
- when(mockClientGenerator.initSelfSignedCAHttpClient(anyString(), any(JSONObject.class))).thenReturn(mockedOkHttpClient);
+ when(mockClientGenerator.initCommercialCAHttpClient(any(JSONObject.class), anyInt())).thenReturn(mockedOkHttpClient);
+ when(mockClientGenerator.initSelfSignedCAHttpClient(anyString(), anyInt(), any(JSONObject.class))).thenReturn(mockedOkHttpClient);
if (resolveDNS) {
when(mockedOkHttpClient.dns().lookup(anyString())).thenReturn(new ArrayList<>());
} else {
@@ -498,6 +569,10 @@ public class MockHelper {
thenReturn(String.format(errorMessages.getString("setup_error_text"), "Bitmask"));
when(mockedResources.getString(R.string.app_name)).
thenReturn("Bitmask");
+ when(mockedResources.getString(eq(R.string.error_tor_timeout), anyString())).
+ thenReturn(String.format(errorMessages.getString("error_tor_timeout"), "Bitmask"));
+ when(mockedResources.getString(eq(R.string.error_network_connection), anyString())).
+ thenReturn(String.format(errorMessages.getString("error_network_connection"), "Bitmask"));
return mockedResources;
}
diff --git a/app/src/test/resources/error_messages.json b/app/src/test/resources/error_messages.json
index e3b92d78..ae04bdb0 100644
--- a/app/src/test/resources/error_messages.json
+++ b/app/src/test/resources/error_messages.json
@@ -13,5 +13,7 @@
"warning_corrupted_provider_details": "Stored provider details are corrupted. You can either update %s (recommended) or update the provider details using a commercial CA certificate.",
"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.",
"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.",
- "setup_error_text": "There was an error configuring %s with your chosen provider."
+ "setup_error_text": "There was an error configuring %s with your chosen provider.",
+ "error_tor_timeout": "Starting bridges failed. Do you want to retry or continue with an unobfuscated secure connection to configure %s?",
+ "error_network_connection": "%s has no internet connection. Please check your WiFi and cellular data settings."
} \ No newline at end of file
diff --git a/app/src/test/resources/v4/riseup.net.json b/app/src/test/resources/v4/riseup.net.json
index 10e12df9..2f921e57 100644
--- a/app/src/test/resources/v4/riseup.net.json
+++ b/app/src/test/resources/v4/riseup.net.json
@@ -1,6 +1,6 @@
{
"api_uri": "https://api.black.riseup.net:443",
- "api_version": "1",
+ "api_version": "4",
"ca_cert_fingerprint": "SHA256: a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494",
"ca_cert_uri": "https://black.riseup.net/ca.crt",
"default_language": "en",