summaryrefslogtreecommitdiff
path: root/app/src/main/java/se/leap/bitmaskclient/tor
diff options
context:
space:
mode:
authorcyberta <cyberta@riseup.net>2021-11-12 00:46:35 +0000
committercyberta <cyberta@riseup.net>2021-11-12 00:46:35 +0000
commitc5d722f555b952407dade3abb1ffd537e6747317 (patch)
treea9ebb8b33438589a33ed9ce54ade50371c9fe147 /app/src/main/java/se/leap/bitmaskclient/tor
parent571c0479f7400e56cfdb27408160d8a816cc8610 (diff)
parent8aeb4791b6e024de9aa9c61b574d8c798a3c0a2c (diff)
Merge branch 'tor-snowflake' into 'master'
tor-over-snowflake Closes #9045 See merge request leap/bitmask_android!138
Diffstat (limited to 'app/src/main/java/se/leap/bitmaskclient/tor')
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/ClientTransportPlugin.java178
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/TorNotificationManager.java128
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/TorServiceCommand.java138
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/TorServiceConnection.java88
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/tor/TorStatusObservable.java290
5 files changed, 822 insertions, 0 deletions
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;
+ }
+}