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