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