diff options
author | cyberta <cyberta@riseup.net> | 2021-11-12 00:46:35 +0000 |
---|---|---|
committer | cyberta <cyberta@riseup.net> | 2021-11-12 00:46:35 +0000 |
commit | c5d722f555b952407dade3abb1ffd537e6747317 (patch) | |
tree | a9ebb8b33438589a33ed9ce54ade50371c9fe147 /app/src/main/java | |
parent | 571c0479f7400e56cfdb27408160d8a816cc8610 (diff) | |
parent | 8aeb4791b6e024de9aa9c61b574d8c798a3c0a2c (diff) |
Merge branch 'tor-snowflake' into 'master'
tor-over-snowflake
Closes #9045
See merge request leap/bitmask_android!138
Diffstat (limited to 'app/src/main/java')
34 files changed, 1443 insertions, 129 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; + } +} |