summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcyBerta <cyberta@riseup.net>2021-06-23 03:27:17 +0200
committercyBerta <cyberta@riseup.net>2021-07-21 22:02:24 +0200
commitfe6a0e47121d17d08c7d913f1db086687a569446 (patch)
tree0b37235a33c490647f6222d5f1cd6072abc34506
parent571c0479f7400e56cfdb27408160d8a816cc8610 (diff)
initial tor-integration to circumvent blocking attempts of the provider api
-rw-r--r--app/build.gradle3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java4
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java1
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java14
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java37
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java134
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPICommand.java18
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java72
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java25
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/TorNotificationManager.java105
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java102
-rw-r--r--app/src/main/res/values/strings.xml5
-rw-r--r--app/src/production/java/se/leap/bitmaskclient/providersetup/ProviderApiManager.java55
13 files changed, 532 insertions, 43 deletions
diff --git a/app/build.gradle b/app/build.gradle
index f1331e12..7f4f667e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -391,6 +391,9 @@ dependencies {
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'de.hdodenhof:circleimageview:3.1.0'
+ implementation 'info.guardianproject:tor-android:0.4.5.7'
+ implementation 'info.guardianproject:jtorctl:0.4.5.7'
+
fatwebImplementation project(path: ':bitmask-web-core')
fatImplementation project(path: ':bitmask-core')
x86Implementation project(path: ':bitmask-core')
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/models/Constants.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
index 3edfbb3d..f627a24e 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
@@ -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/utils/PreferenceHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
index a3d1314e..cbea2815 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
@@ -34,6 +34,7 @@ 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_TOR;
/**
* Created by cyberta on 18.03.18.
@@ -212,6 +213,18 @@ public class PreferenceHelper {
putString(context, PREFERRED_CITY, city);
}
+ public static Boolean useTor(SharedPreferences preferences) {
+ return preferences.getBoolean(USE_TOR, true);
+ }
+
+ public static boolean useTor(Context context) {
+ return getBoolean(context, USE_TOR, true);
+ }
+
+ public static void setUseTor(Context context, boolean useTor) {
+ putBoolean(context, USE_TOR, useTor);
+ }
+
public static JSONObject getEipDefinitionFromPreferences(SharedPreferences preferences) {
JSONObject result = new JSONObject();
try {
@@ -278,5 +291,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/EipSetupObserver.java b/app/src/main/java/se/leap/bitmaskclient/eip/EipSetupObserver.java
index 1ad5f7d2..45c829b6 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,29 @@ 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.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;
@@ -67,7 +69,8 @@ import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOAD
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_GEOIP_JSON;
-import static se.leap.bitmaskclient.appUpdate.DownloadServiceCommand.CHECK_VERSION_FILE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.STOP_PROXY;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus.OFF;
/**
* Created by cyberta on 05.12.18.
@@ -92,6 +95,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 +145,30 @@ 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);
+ TorStatusObservable.updateState(context, status);
+ }
+
+
private void handleProviderApiEvent(Intent intent) {
int resultCode = intent.getIntExtra(BROADCAST_RESULT_CODE, RESULT_CANCELED);
Bundle resultData = intent.getParcelableExtra(BROADCAST_RESULT_KEY);
@@ -324,6 +348,9 @@ public class EipSetupObserver extends BroadcastReceiver implements VpnStatus.Sta
setupNClosestGateway.set(0);
observedProfileFromVpnStatus = null;
this.changingGateway.set(changingGateway);
+ if (TorStatusObservable.getStatus() != OFF) {
+ ProviderAPICommand.execute(context.getApplicationContext(), STOP_PROXY, null);
+ }
}
/**
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..dcbe9636 100644
--- a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java
@@ -17,17 +17,36 @@
package se.leap.bitmaskclient.providersetup;
import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.ServiceConnection;
import android.content.SharedPreferences;
+import android.os.Build;
+import android.os.IBinder;
+import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.JobIntentService;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+import org.torproject.jni.TorService;
+
+import java.io.Closeable;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import se.leap.bitmaskclient.base.utils.PreferenceHelper;
import se.leap.bitmaskclient.providersetup.connectivity.OkHttpClientGenerator;
+import se.leap.bitmaskclient.tor.TorNotificationManager;
+import se.leap.bitmaskclient.tor.TorStatusObservable;
import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.ensureNotOnMainThread;
+import static se.leap.bitmaskclient.tor.TorNotificationManager.TOR_SERVICE_NOTIFICATION_ID;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus.OFF;
+import static se.leap.bitmaskclient.tor.TorStatusObservable.TorStatus.STOPPING;
/**
* Implements HTTP api methods (encapsulated in {{@link ProviderApiManager}})
@@ -49,6 +68,7 @@ public class ProviderAPI extends JobIntentService implements ProviderApiManagerB
final public static String
TAG = ProviderAPI.class.getSimpleName(),
+ STOP_PROXY = "stopProxy",
SET_UP_PROVIDER = "setUpProvider",
UPDATE_PROVIDER_DETAILS = "updateProviderDetails",
DOWNLOAD_GEOIP_JSON = "downloadGeoIpJson",
@@ -85,6 +105,7 @@ public class ProviderAPI extends JobIntentService implements ProviderApiManagerB
INCORRECTLY_DOWNLOADED_GEOIP_JSON = 18;
ProviderApiManager providerApiManager;
+ private volatile TorServiceConnection torServiceConnection;
//TODO: refactor me, please!
//used in insecure flavor only
@@ -116,13 +137,126 @@ public class ProviderAPI extends JobIntentService implements ProviderApiManagerB
}
@Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (torServiceConnection != null) {
+ torServiceConnection.close();
+ torServiceConnection = null;
+ }
+ }
+
+ @Override
public void broadcastEvent(Intent intent) {
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}
+
+
+ @Override
+ public int initTorConnection() {
+ initTorServiceConnection(this);
+ if (torServiceConnection != null) {
+ Intent torServiceIntent = new Intent(this, TorService.class);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ Notification notification = TorNotificationManager.buildTorForegroundNotification(getApplicationContext());
+ //noinspection NewApi
+ getApplicationContext().startForegroundService(torServiceIntent);
+ torServiceConnection.torService.startForeground(TOR_SERVICE_NOTIFICATION_ID, notification);
+ } else {
+ getApplicationContext().startService(torServiceIntent);
+ }
+
+ return torServiceConnection.torService.getHttpTunnelPort();
+ }
+
+ return -1;
+ }
+
+ @Override
+ public void stopTorConnection() {
+ if (TorStatusObservable.getStatus() != OFF) {
+ TorStatusObservable.updateState(this, STOPPING.toString());
+ initTorServiceConnection(this);
+ if (torServiceConnection != null) {
+ torServiceConnection.torService.stopSelf();
+ }
+ }
+ }
+
private ProviderApiManager initApiManager() {
SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
OkHttpClientGenerator clientGenerator = new OkHttpClientGenerator(getResources());
return new ProviderApiManager(preferences, getResources(), clientGenerator, this);
}
+
+ /**
+ * Assigns a new TorServiceConnection to ProviderAPI's member variable torServiceConnection.
+ * Only one thread at a time can create the service connection, that will be shared between threads
+ *
+ * @throws InterruptedException thrown if thread gets interrupted
+ * @throws IllegalStateException thrown if this method was not called from a background thread
+ */
+ private void initTorServiceConnection(Context context) {
+ if (PreferenceHelper.useTor(context)) {
+ try {
+ if (torServiceConnection == null) {
+ Log.d(TAG, "serviceConnection is still null");
+ torServiceConnection = new TorServiceConnection(context);
+ }
+ } catch (InterruptedException | IllegalStateException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public static class TorServiceConnection implements Closeable {
+ private final Context context;
+ private ServiceConnection serviceConnection;
+ private TorService torService;
+
+ TorServiceConnection(Context context) throws InterruptedException, IllegalStateException {
+ this.context = context;
+ ensureNotOnMainThread(context);
+ Log.d(TAG, "initSynchronizedServiceConnection!");
+ initSynchronizedServiceConnection(context);
+ }
+
+ @Override
+ public void close() {
+ context.unbindService(serviceConnection);
+ }
+
+ private void initSynchronizedServiceConnection(final Context context) throws InterruptedException {
+ 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;
+ try {
+ TorService.LocalBinder binder = (TorService.LocalBinder) service;
+ blockingQueue.put(binder.getService());
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ }
+ };
+ 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/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/ProviderApiManagerBase.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java
index c5dc6572..8118c872 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,9 @@ import java.security.interfaces.RSAPrivateKey;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
+import java.util.Observer;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLPeerUnverifiedException;
@@ -59,10 +62,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;
@@ -119,6 +125,7 @@ import static se.leap.bitmaskclient.providersetup.ProviderAPI.PROVIDER_OK;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.RECEIVER_KEY;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.SET_UP_PROVIDER;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.SIGN_UP;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.STOP_PROXY;
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;
@@ -128,6 +135,8 @@ 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.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 +149,11 @@ public abstract class ProviderApiManagerBase {
public interface ProviderApiServiceCallback {
void broadcastEvent(Intent intent);
+ int initTorConnection();
+ void stopTorConnection();
}
- private ProviderApiServiceCallback serviceCallback;
+ private final ProviderApiServiceCallback serviceCallback;
protected SharedPreferences preferences;
protected Resources resources;
@@ -164,17 +175,22 @@ 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 if (!STOP_PROXY.equals(action)) {
//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!");
- return;
- }
+
+ // uncomment for testing --v
+ TorStatusObservable.setProxyPort(startTorProxy());
Bundle result = new Bundle();
switch (action) {
@@ -269,9 +285,43 @@ public abstract class ProviderApiManagerBase {
}
ProviderObservable.getInstance().setProviderForDns(null);
}
+ break;
+ case STOP_PROXY:
+ serviceCallback.stopTorConnection();
+ break;
}
}
+ protected int startTorProxy() {
+ int port = -1;
+ if (PreferenceHelper.useTor(preferences) && EipStatus.getInstance().isDisconnected() ) {
+ port = serviceCallback.initTorConnection();
+ if (port != -1) {
+ try {
+ waitForTorCircuits();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ port = -1;
+ }
+ }
+ }
+ return port;
+ }
+
+ private void waitForTorCircuits() throws InterruptedException {
+ if (TorStatusObservable.getStatus() == ON) {
+ return;
+ }
+ CountDownLatch countDownLatch = new CountDownLatch(1);
+ Observer observer = (o, arg) -> {
+ if (TorStatusObservable.getStatus() == ON) {
+ countDownLatch.countDown();
+ }
+ };
+ TorStatusObservable.getInstance().addObserver(observer);
+ countDownLatch.await(90, TimeUnit.SECONDS);
+ }
+
void resetProviderDetails(Provider provider) {
provider.reset();
deleteProviderDetailsFromPreferences(preferences, provider.getDomain());
@@ -342,7 +392,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 +451,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);
}
@@ -681,7 +731,7 @@ public abstract class ProviderApiManagerBase {
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;
@@ -950,7 +1000,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/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/TorNotificationManager.java b/app/src/main/java/se/leap/bitmaskclient/tor/TorNotificationManager.java
new file mode 100644
index 00000000..71a2735c
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/tor/TorNotificationManager.java
@@ -0,0 +1,105 @@
+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.annotation.NonNull;
+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";
+
+
+ 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())
+ .setContentTitle("Using Bridges to configure provider.").build();
+ }
+
+ public void buildTorNotification(Context context, String state) {
+ NotificationManager notificationManager = initNotificationManager(context);
+ if (notificationManager == null) {
+ return;
+ }
+ NotificationCompat.Builder notificationBuilder = initNotificationBuilderDefaults(context);
+ notificationBuilder
+ .setSmallIcon(R.drawable.ic_bridge_36)
+ .setWhen(System.currentTimeMillis())
+ .setTicker(state)
+ .setContentTitle(context.getString(R.string.tor_provider_setup))
+ .setContentText(state);
+ notificationManager.notify(TOR_SERVICE_NOTIFICATION_ID, notificationBuilder.build());
+ }
+
+
+ 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(notificationManager);
+ }
+ return notificationManager;
+ }
+
+ @TargetApi(26)
+ private static void createNotificationChannel(NotificationManager notificationManager) {
+ CharSequence name = "Bitmask Tor Service";
+ String description = "Informs about usage of bridges to configure Bitmask.";
+ 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).
+ setAutoCancel(true);
+ 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/TorStatusObservable.java b/app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java
new file mode 100644
index 00000000..ed4ae24b
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java
@@ -0,0 +1,102 @@
+package se.leap.bitmaskclient.tor;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Observable;
+
+import se.leap.bitmaskclient.R;
+
+public class TorStatusObservable extends Observable {
+
+ private static final String TAG = TorStatusObservable.class.getSimpleName();
+
+ public enum TorStatus {
+ ON,
+ OFF,
+ STARTING,
+ STOPPING,
+ UNKOWN
+ }
+
+ private static TorStatusObservable instance;
+ private TorStatus status = TorStatus.UNKOWN;
+ private final TorNotificationManager torNotificationManager;
+ private String lastError;
+ private int port = -1;
+
+ 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 void updateState(Context context, String status) {
+ try {
+ Log.d(TAG, "update tor state: " + status);
+ getInstance().status = TorStatus.valueOf(status);
+ if (getInstance().status == TorStatus.OFF) {
+ getInstance().torNotificationManager.cancelNotifications(context);
+ } else {
+ getInstance().torNotificationManager.buildTorNotification(context, getStringForCurrentStatus(context));
+ }
+ instance.setChanged();
+ instance.notifyObservers();
+
+
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ }
+ }
+
+ 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 String getLastError() {
+ return lastError;
+ }
+
+ private 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:
+ case UNKOWN:
+ break;
+ }
+ return null;
+ }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e883b974..b43a0683 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -155,4 +155,9 @@
<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 Tor with bridges.</string>
+ <string name="tor_stopping">Stopping Tor with bridges.</string>
+ <string name="tor_started">Running Tor with bridges to fetch provider configuration.</string>
+ <string name="tor_provider_setup">Using Bridges to configure provider.</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..b6069982 100644
--- a/app/src/production/java/se/leap/bitmaskclient/providersetup/ProviderApiManager.java
+++ b/app/src/production/java/se/leap/bitmaskclient/providersetup/ProviderApiManager.java
@@ -34,8 +34,11 @@ 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 +55,9 @@ 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.TorStatus.UNKOWN;
+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;
}
@@ -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, 0);
+ }
+
+ /**
+ * Tries to download the contents of the provided url using commercially validated CA certificate from chosen provider.
+ *
+ */
+ private String downloadWithCommercialCA(String stringUrl, Provider provider, int tries) {
+
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,17 @@ public class ProviderApiManager extends ProviderApiManagerBase {
}
}
+ if (tries == 0 &&
+ responseString != null &&
+ responseString.contains(ERRORS) &&
+ PreferenceHelper.useTor(preferences) &&
+ EipStatus.getInstance().isDisconnected() &&
+ TorStatusObservable.getStatus() == OFF ||
+ TorStatusObservable.getStatus() == UNKOWN) {
+ TorStatusObservable.setProxyPort(startTorProxy());
+ return downloadWithCommercialCA(stringUrl, provider, 1);
+ }
+
return responseString;
}
@@ -330,9 +352,13 @@ public class ProviderApiManager extends ProviderApiManagerBase {
}
private String downloadFromUrlWithProviderCA(String urlString, Provider provider) {
+ return downloadFromUrlWithProviderCA(urlString, provider, 0);
+ }
+
+ private String downloadFromUrlWithProviderCA(String urlString, Provider provider, int tries) {
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 +366,17 @@ public class ProviderApiManager extends ProviderApiManagerBase {
List<Pair<String, String>> headerArgs = getAuthorizationHeader();
responseString = sendGetStringToServer(urlString, headerArgs, okHttpClient);
+ if (tries == 0 &&
+ responseString != null &&
+ responseString.contains(ERRORS) &&
+ PreferenceHelper.useTor(preferences) &&
+ EipStatus.getInstance().isDisconnected() &&
+ TorStatusObservable.getStatus() == OFF ||
+ TorStatusObservable.getStatus() == UNKOWN) {
+ TorStatusObservable.setProxyPort(startTorProxy());
+ return downloadFromUrlWithProviderCA(urlString, provider, 1);
+ }
+
return responseString;
}
@@ -354,7 +391,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();
}