diff --git a/build.gradle b/build.gradle index eabaadf71500307088f57fc30cce431bbe8936a0..ec2738b5e402e3bb8403cdeb95c93914e1e3dedf 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.2' + classpath 'com.android.tools.build:gradle:3.3.3' } } diff --git a/sampletorapp/src/main/AndroidManifest.xml b/sampletorapp/src/main/AndroidManifest.xml index ba3c431145234093ec04a0e7359896ceb7dcbb19..fef34e7235410862aa1d15a709d57ef32fd7027d 100644 --- a/sampletorapp/src/main/AndroidManifest.xml +++ b/sampletorapp/src/main/AndroidManifest.xml @@ -13,7 +13,8 @@ android:supportsRtl="true" android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning"> - <activity android:name=".MainActivity"> + <activity android:name=".MainActivity" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> diff --git a/tor-android-binary/build.gradle b/tor-android-binary/build.gradle index d2e361304945241efd96426a562505c1d324d0f3..be111fc0cc0b252813db6ce0e4eb36cd08e98a17 100644 --- a/tor-android-binary/build.gradle +++ b/tor-android-binary/build.gradle @@ -47,9 +47,10 @@ dependencies { api 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' api 'info.guardianproject:jtorctl:0.4.2.5' - androidTestImplementation 'androidx.test:core:1.2.0' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test:rules:1.2.0' + androidTestImplementation 'androidx.test:core:1.4.0' + androidTestImplementation 'androidx.test:runner:1.4.0' + androidTestImplementation 'androidx.test:rules:1.4.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'info.guardianproject.netcipher:netcipher:2.1.0' androidTestImplementation 'commons-io:commons-io:2.6' androidTestImplementation 'commons-net:commons-net:3.6' diff --git a/tor-android-binary/src/androidTest/java/org/torproject/jni/TorServiceDoubleUnbindTest.java b/tor-android-binary/src/androidTest/java/org/torproject/jni/TorServiceDoubleUnbindTest.java new file mode 100644 index 0000000000000000000000000000000000000000..3857b58bae6e8658edd1f0ff4948337cfa51e3be --- /dev/null +++ b/tor-android-binary/src/androidTest/java/org/torproject/jni/TorServiceDoubleUnbindTest.java @@ -0,0 +1,89 @@ +package org.torproject.jni; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static org.torproject.jni.TorService.ACTION_STATUS; +import static org.torproject.jni.TorService.EXTRA_STATUS; +import static org.torproject.jni.TorService.STATUS_OFF; +import static org.torproject.jni.TorService.STATUS_ON; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.util.Log; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.rule.ServiceTestRule; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@RunWith(AndroidJUnit4.class) +public class TorServiceDoubleUnbindTest { + + public static final String TAG = "TorServiceTest"; + + @Rule + public final ServiceTestRule serviceRule = ServiceTestRule.withTimeout(120L, TimeUnit.SECONDS); + + private Context context; + + @Before + public void setUp() { + context = getInstrumentation().getTargetContext(); + } + + /** + * Test using {@link ServiceTestRule#bindService(Intent, ServiceConnection, int)} + * for reliable start/stop when testing. + */ + @Test + public void testBindService() throws Exception { + startAndUnbind(); + startAndUnbind(); + } + + private void startAndUnbind() throws Exception { + final CountDownLatch startedLatch = new CountDownLatch(1); + final CountDownLatch stoppedLatch = new CountDownLatch(1); + BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String status = intent.getStringExtra(EXTRA_STATUS); + Log.i(TAG, "receiver.onReceive: " + status + " " + intent); + if (STATUS_ON.equals(status)) { + startedLatch.countDown(); + } else if (STATUS_OFF.equals(status)) { + stoppedLatch.countDown(); + } + } + }; + // run the BroadcastReceiver in its own thread + HandlerThread handlerThread = new HandlerThread(receiver.getClass().getSimpleName()); + handlerThread.start(); + Looper looper = handlerThread.getLooper(); + Handler handler = new Handler(looper); + context.registerReceiver(receiver, new IntentFilter(ACTION_STATUS), null, handler); + + Intent serviceIntent = new Intent(context, TorService.class); + IBinder binder = serviceRule.bindService(serviceIntent); + TorService torService = ((TorService.LocalBinder) binder).getService(); + startedLatch.await(); + + serviceRule.unbindService(); + stoppedLatch.await(); + + context.unregisterReceiver(receiver); + } + +} diff --git a/tor-android-binary/src/androidTest/java/org/torproject/jni/TorServiceTest.java b/tor-android-binary/src/androidTest/java/org/torproject/jni/TorServiceTest.java index d50f47483d145735b71312460ed3662b372d9e49..5bd2a35eb62794e732a0dc3e4184127d089c6301 100644 --- a/tor-android-binary/src/androidTest/java/org/torproject/jni/TorServiceTest.java +++ b/tor-android-binary/src/androidTest/java/org/torproject/jni/TorServiceTest.java @@ -155,7 +155,7 @@ public class TorServiceTest { assertTrue("NetCipher.getHttpURLConnection should use Tor", NetCipher.isNetCipherGetHttpURLConnectionUsingTor()); - URLConnection c = NetCipher.getHttpsURLConnection("https://www.nytimes3xbfgragh.onion/"); + URLConnection c = NetCipher.getHttpsURLConnection("https://www.nytimesn7cgmftshazwhfgzm37qxb44r64ytbb2dj3x62d2lljsciiyd.onion/"); Log.i(TAG, "Content-Length: " + c.getContentLength()); Log.i(TAG, "CONTENTS: " + new String(IOUtils.readFully(c.getInputStream(), 100))); @@ -174,6 +174,7 @@ public class TorServiceTest { FileUtils.write(torrc, dnsPort + " " + testValue + "\n"); final CountDownLatch startedLatch = new CountDownLatch(1); + final CountDownLatch stoppedLatch = new CountDownLatch(1); BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -185,6 +186,8 @@ public class TorServiceTest { Log.i(TAG, "receiver.onReceive: " + status + " " + intent); if (TorService.STATUS_ON.equals(status)) { startedLatch.countDown(); + } else if (TorService.STATUS_OFF.equals(status)) { + stoppedLatch.countDown(); } } }; @@ -203,12 +206,14 @@ public class TorServiceTest { assertEquals(testValue, getConf(torService.getTorControlConnection(), dnsPort)); serviceRule.unbindService(); + stoppedLatch.await(); } @Test public void testDownloadingLargeFile() throws TimeoutException, InterruptedException, IOException { Assume.assumeTrue("Only works on Android 7.1.2 or higher", Build.VERSION.SDK_INT >= 24); final CountDownLatch startedLatch = new CountDownLatch(1); + final CountDownLatch stoppedLatch = new CountDownLatch(1); BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -220,6 +225,8 @@ public class TorServiceTest { Log.i(TAG, "receiver.onReceive: " + status + " " + intent); if (TorService.STATUS_ON.equals(status)) { startedLatch.countDown(); + } else if (TorService.STATUS_OFF.equals(status)) { + stoppedLatch.countDown(); } } }; @@ -242,7 +249,7 @@ public class TorServiceTest { // ~350MB //URL url = new URL("http://dl.google.com/android/ndk/android-ndk-r9b-linux-x86_64.tar.bz2"); // ~3MB - URL url = new URL("http://dl.google.com/android/repository/platform-tools_r24-linux.zip"); + URL url = new URL("https://dl.google.com/android/repository/platform-tools_r24-linux.zip"); // 55KB //URL url = new URL("https://jcenter.bintray.com/com/android/tools/build/gradle/2.2.3/gradle-2.2.3.jar"); HttpURLConnection connection = NetCipher.getHttpURLConnection(url); @@ -252,6 +259,7 @@ public class TorServiceTest { IOUtils.copy(connection.getInputStream(), new FileWriter(new File("/dev/null"))); serviceRule.unbindService(); + stoppedLatch.await(); } private static boolean canConnectToSocket(String host, int port) { diff --git a/tor-android-binary/src/main/java/org/torproject/jni/TorService.java b/tor-android-binary/src/main/java/org/torproject/jni/TorService.java index b34cef707a730f063891df6ac92c2fb125574701..affdca0413922b0acfe4f0de56a016460140d66c 100644 --- a/tor-android-binary/src/main/java/org/torproject/jni/TorService.java +++ b/tor-android-binary/src/main/java/org/torproject/jni/TorService.java @@ -7,6 +7,7 @@ import android.os.Binder; import android.os.FileObserver; import android.os.IBinder; import android.os.Process; +import android.util.Log; import net.freehaven.tor.control.RawEventListener; import net.freehaven.tor.control.TorControlCommands; @@ -24,6 +25,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; import androidx.annotation.Nullable; import androidx.localbroadcastmanager.content.LocalBroadcastManager; @@ -167,6 +169,12 @@ public class TorService extends Service { private TorControlConnection torControlConnection; + /** + * This lock must be acquired before calling createTorConfiguration() and + * held until mainConfigurationFree() has been called. + */ + private static final ReentrantLock runLock = new ReentrantLock(); + private native String apiGetProviderVersion(); private native boolean createTorConfiguration(); @@ -314,50 +322,61 @@ public class TorService extends Service { httpTunnelPort = Integer.toString(8118); } - createTorConfiguration(); - ArrayList<String> lines = new ArrayList<>(Arrays.asList("tor", "--verify-config", // must always be here - "--RunAsDaemon", "0", - "-f", getTorrc(context).getAbsolutePath(), - "--defaults-torrc", getDefaultsTorrc(context).getAbsolutePath(), - "--ignore-missing-torrc", - "--SyslogIdentityTag", TAG, - "--CacheDirectory", new File(getCacheDir(), TAG).getAbsolutePath(), - "--DataDirectory", getAppTorServiceDataDir(context).getAbsolutePath(), - "--ControlSocket", getControlSocket(context).getAbsolutePath(), - "--CookieAuthentication", "0", - "--SOCKSPort", socksPort, - "--HTTPTunnelPort", httpTunnelPort, - - // can be moved to ControlPort messages - "--LogMessageDomains", "1", - "--TruncateLogFile", "1" - )); - String[] verifyLines = lines.toArray(new String[0]); - if (!mainConfigurationSetCommandLine(verifyLines)) { - throw new IllegalArgumentException("Setting command line failed: " + Arrays.toString(verifyLines)); - } - int result = runMain(); // run verify - if (result != 0) { - throw new IllegalArgumentException("Bad command flags: " + Arrays.toString(verifyLines)); + if (runLock.isLocked()) { + Log.i(TAG, "Waiting for lock"); } + runLock.lock(); + Log.i(TAG, "Acquired lock"); + try { + createTorConfiguration(); + ArrayList<String> lines = new ArrayList<>(Arrays.asList("tor", "--verify-config", // must always be here + "--RunAsDaemon", "0", + "-f", getTorrc(context).getAbsolutePath(), + "--defaults-torrc", getDefaultsTorrc(context).getAbsolutePath(), + "--ignore-missing-torrc", + "--SyslogIdentityTag", TAG, + "--CacheDirectory", new File(getCacheDir(), TAG).getAbsolutePath(), + "--DataDirectory", getAppTorServiceDataDir(context).getAbsolutePath(), + "--ControlSocket", getControlSocket(context).getAbsolutePath(), + "--CookieAuthentication", "0", + "--SOCKSPort", socksPort, + "--HTTPTunnelPort", httpTunnelPort, + + // can be moved to ControlPort messages + "--LogMessageDomains", "1", + "--TruncateLogFile", "1" + )); + String[] verifyLines = lines.toArray(new String[0]); + if (!mainConfigurationSetCommandLine(verifyLines)) { + throw new IllegalArgumentException("Setting command line failed: " + Arrays.toString(verifyLines)); + } + int result = runMain(); // run verify + if (result != 0) { + throw new IllegalArgumentException("Bad command flags: " + Arrays.toString(verifyLines)); + } - controlPortThreadStarted = new CountDownLatch(1); - controlPortThread.start(); - controlPortThreadStarted.await(); + controlPortThreadStarted = new CountDownLatch(1); + controlPortThread.start(); + controlPortThreadStarted.await(); - String[] runLines = new String[lines.size() - 1]; - runLines[0] = "tor"; - for (int i = 2; i < lines.size(); i++) { - runLines[i - 1] = lines.get(i); - } - if (!mainConfigurationSetCommandLine(runLines)) { - throw new IllegalArgumentException("Setting command line failed: " + Arrays.toString(runLines)); - } - if (!mainConfigurationSetupControlSocket()) { - throw new IllegalStateException("Setting up ControlPort failed!"); - } - if (runMain() != 0) { - throw new IllegalStateException("Tor could not start!"); + String[] runLines = new String[lines.size() - 1]; + runLines[0] = "tor"; + for (int i = 2; i < lines.size(); i++) { + runLines[i - 1] = lines.get(i); + } + if (!mainConfigurationSetCommandLine(runLines)) { + throw new IllegalArgumentException("Setting command line failed: " + Arrays.toString(runLines)); + } + if (!mainConfigurationSetupControlSocket()) { + throw new IllegalStateException("Setting up ControlPort failed!"); + } + if (runMain() != 0) { + throw new IllegalStateException("Tor could not start!"); + } + } finally { + mainConfigurationFree(); + Log.i(TAG, "Releasing lock"); + runLock.unlock(); } } catch (IllegalStateException | IllegalArgumentException | InterruptedException e) { @@ -377,7 +396,6 @@ public class TorService extends Service { torControlConnection.removeRawEventListener(startedEventListener); } shutdownTor(TorControlCommands.SIGNAL_SHUTDOWN); - mainConfigurationFree(); broadcastStatus(TorService.this, STATUS_OFF); }