From fe6a0e47121d17d08c7d913f1db086687a569446 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Wed, 23 Jun 2021 03:27:17 +0200 Subject: initial tor-integration to circumvent blocking attempts of the provider api --- .../bitmaskclient/providersetup/ProviderAPI.java | 134 +++++++++++++++++++++ .../providersetup/ProviderAPICommand.java | 18 +-- .../providersetup/ProviderApiManagerBase.java | 72 +++++++++-- .../connectivity/OkHttpClientGenerator.java | 25 ++-- 4 files changed, 221 insertions(+), 28 deletions(-) (limited to 'app/src/main/java/se/leap/bitmaskclient/providersetup') 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 @@ -115,14 +136,127 @@ public class ProviderAPI extends JobIntentService implements ProviderApiManagerB providerApiManager.handleIntent(command); } + @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 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(); } -- cgit v1.2.3