Merge branch 'bluetooth-race' into 'master'

Fix race condition when closing redundant Bluetooth sockets

The Bluetooth invitation code has a race condition: if Alice and Bob connect to each other at roughly the same time, they each consider their outgoing socket to be redundant and close it, resulting in both sockets being closed. This can be triggered pretty reliably by using two phones of the same model and pressing 'Continue' at the same time on both phones.

When more than one invitation socket is opened, Alice should pick which one to use and Bob should use whichever one Alice picks, which Bob can detect by trying to read from both sockets.

Hopefully the Bluetooth invitation code will be retired when #117 is merged, but I'm putting this up for review in case we need to keep Bluetooth as a fallback method.

See merge request !120
This commit is contained in:
akwizgran
2016-03-09 10:12:05 +00:00
15 changed files with 230 additions and 532 deletions

View File

@@ -19,11 +19,10 @@ import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback; import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
import org.briarproject.api.plugins.duplex.DuplexTransportConnection; import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
import org.briarproject.api.properties.TransportProperties; import org.briarproject.api.properties.TransportProperties;
import org.briarproject.api.system.Clock;
import org.briarproject.util.LatchedReference;
import org.briarproject.util.StringUtils; import org.briarproject.util.StringUtils;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@@ -33,9 +32,12 @@ import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.Future;
import java.util.logging.Logger; import java.util.logging.Logger;
import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED; import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED;
@@ -69,7 +71,6 @@ class DroidtoothPlugin implements DuplexPlugin {
private final AndroidExecutor androidExecutor; private final AndroidExecutor androidExecutor;
private final Context appContext; private final Context appContext;
private final SecureRandom secureRandom; private final SecureRandom secureRandom;
private final Clock clock;
private final Backoff backoff; private final Backoff backoff;
private final DuplexPluginCallback callback; private final DuplexPluginCallback callback;
private final int maxLatency; private final int maxLatency;
@@ -83,13 +84,12 @@ class DroidtoothPlugin implements DuplexPlugin {
private volatile BluetoothAdapter adapter = null; private volatile BluetoothAdapter adapter = null;
DroidtoothPlugin(Executor ioExecutor, AndroidExecutor androidExecutor, DroidtoothPlugin(Executor ioExecutor, AndroidExecutor androidExecutor,
Context appContext, SecureRandom secureRandom, Clock clock, Context appContext, SecureRandom secureRandom, Backoff backoff,
Backoff backoff, DuplexPluginCallback callback, int maxLatency) { DuplexPluginCallback callback, int maxLatency) {
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
this.androidExecutor = androidExecutor; this.androidExecutor = androidExecutor;
this.appContext = appContext; this.appContext = appContext;
this.secureRandom = secureRandom; this.secureRandom = secureRandom;
this.clock = clock;
this.backoff = backoff; this.backoff = backoff;
this.callback = callback; this.callback = callback;
this.maxLatency = maxLatency; this.maxLatency = maxLatency;
@@ -339,7 +339,7 @@ class DroidtoothPlugin implements DuplexPlugin {
} }
public DuplexTransportConnection createInvitationConnection(PseudoRandom r, public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout) { long timeout, boolean alice) {
if (!isRunning()) return null; if (!isRunning()) return null;
// Use the invitation codes to generate the UUID // Use the invitation codes to generate the UUID
byte[] b = r.nextBytes(UUID_BYTES); byte[] b = r.nextBytes(UUID_BYTES);
@@ -353,23 +353,68 @@ class DroidtoothPlugin implements DuplexPlugin {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null; return null;
} }
// Start the background threads // Create the background tasks
LatchedReference<BluetoothSocket> socketLatch = CompletionService<BluetoothSocket> complete =
new LatchedReference<BluetoothSocket>(); new ExecutorCompletionService<BluetoothSocket>(ioExecutor);
new DiscoveryThread(socketLatch, uuid.toString(), timeout).start(); List<Future<BluetoothSocket>> futures =
new BluetoothListenerThread(socketLatch, ss).start(); new ArrayList<Future<BluetoothSocket>>();
// Wait for an incoming or outgoing connection if (alice) {
try { // Return the first connected socket
BluetoothSocket s = socketLatch.waitForReference(timeout); futures.add(complete.submit(new ListeningTask(ss)));
if (s != null) return new DroidtoothTransportConnection(this, s); futures.add(complete.submit(new DiscoveryTask(uuid.toString())));
} catch (InterruptedException e) { } else {
LOG.warning("Interrupted while exchanging invitations"); // Return the first socket with readable data
Thread.currentThread().interrupt(); futures.add(complete.submit(new ReadableTask(
} finally { new ListeningTask(ss))));
// Closing the socket will terminate the listener thread futures.add(complete.submit(new ReadableTask(
tryToClose(ss); new DiscoveryTask(uuid.toString()))));
} }
return null; BluetoothSocket chosen = null;
try {
Future<BluetoothSocket> f = complete.poll(timeout, MILLISECONDS);
if (f == null) return null; // No task completed within the timeout
chosen = f.get();
return new DroidtoothTransportConnection(this, chosen);
} catch (InterruptedException e) {
LOG.info("Interrupted while exchanging invitations");
Thread.currentThread().interrupt();
return null;
} catch (ExecutionException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null;
} finally {
// Closing the socket will terminate the listener task
tryToClose(ss);
closeSockets(futures, chosen);
}
}
private void closeSockets(final List<Future<BluetoothSocket>> futures,
final BluetoothSocket chosen) {
ioExecutor.execute(new Runnable() {
public void run() {
for (Future<BluetoothSocket> f : futures) {
try {
if (f.cancel(true)) {
LOG.info("Cancelled task");
} else {
BluetoothSocket s = f.get();
if (s != null && s != chosen) {
LOG.info("Closing unwanted socket");
s.close();
}
}
} catch (InterruptedException e) {
LOG.info("Interrupted while closing sockets");
return;
} catch (ExecutionException e) {
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
} catch (IOException e) {
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
}
}
}
});
} }
private class BluetoothStateReceiver extends BroadcastReceiver { private class BluetoothStateReceiver extends BroadcastReceiver {
@@ -395,61 +440,37 @@ class DroidtoothPlugin implements DuplexPlugin {
} }
} }
private class DiscoveryThread extends Thread { private class DiscoveryTask implements Callable<BluetoothSocket> {
private final LatchedReference<BluetoothSocket> socketLatch;
private final String uuid; private final String uuid;
private final long timeout;
private DiscoveryThread(LatchedReference<BluetoothSocket> socketLatch, private DiscoveryTask(String uuid) {
String uuid, long timeout) {
this.socketLatch = socketLatch;
this.uuid = uuid; this.uuid = uuid;
this.timeout = timeout;
} }
@Override @Override
public void run() { public BluetoothSocket call() throws Exception {
long end = clock.currentTimeMillis() + timeout; // Repeat discovery until we connect or get interrupted
while (!finished(end)) { while (true) {
// Discover nearby devices // Discover nearby devices
LOG.info("Discovering nearby devices"); LOG.info("Discovering nearby devices");
List<String> addresses; List<String> addresses = discoverDevices();
try {
long now = clock.currentTimeMillis();
addresses = discoverDevices(end - now);
} catch (InterruptedException e) {
LOG.warning("Interrupted while discovering devices");
Thread.currentThread().interrupt();
return;
}
if (addresses.isEmpty()) { if (addresses.isEmpty()) {
LOG.info("No devices discovered"); LOG.info("No devices discovered");
continue; continue;
} }
// Connect to any device with the right UUID // Connect to any device with the right UUID
for (String address : addresses) { for (String address : addresses) {
if (finished(end)) return;
BluetoothSocket s = connect(address, uuid); BluetoothSocket s = connect(address, uuid);
if (s != null) { if (s != null) {
LOG.info("Outgoing connection"); LOG.info("Outgoing connection");
if (!socketLatch.set(s)) { return s;
LOG.info("Closing redundant connection");
tryToClose(s);
}
return;
} }
} }
} }
} }
private boolean finished(long end) { private List<String> discoverDevices() throws InterruptedException {
long now = clock.currentTimeMillis();
return now >= end || !isRunning() || socketLatch.isSet();
}
private List<String> discoverDevices(long timeout)
throws InterruptedException {
IntentFilter filter = new IntentFilter(); IntentFilter filter = new IntentFilter();
filter.addAction(FOUND); filter.addAction(FOUND);
filter.addAction(DISCOVERY_FINISHED); filter.addAction(DISCOVERY_FINISHED);
@@ -457,7 +478,7 @@ class DroidtoothPlugin implements DuplexPlugin {
appContext.registerReceiver(disco, filter); appContext.registerReceiver(disco, filter);
LOG.info("Starting discovery"); LOG.info("Starting discovery");
adapter.startDiscovery(); adapter.startDiscovery();
return disco.waitForAddresses(timeout); return disco.waitForAddresses();
} }
} }
@@ -481,38 +502,47 @@ class DroidtoothPlugin implements DuplexPlugin {
} }
} }
private List<String> waitForAddresses(long timeout) private List<String> waitForAddresses() throws InterruptedException {
throws InterruptedException { finished.await();
finished.await(timeout, MILLISECONDS); Collections.shuffle(addresses);
return Collections.unmodifiableList(addresses); return Collections.unmodifiableList(addresses);
} }
} }
private static class BluetoothListenerThread extends Thread { private static class ListeningTask implements Callable<BluetoothSocket> {
private final LatchedReference<BluetoothSocket> socketLatch;
private final BluetoothServerSocket serverSocket; private final BluetoothServerSocket serverSocket;
private BluetoothListenerThread( private ListeningTask(BluetoothServerSocket serverSocket) {
LatchedReference<BluetoothSocket> socketLatch,
BluetoothServerSocket serverSocket) {
this.socketLatch = socketLatch;
this.serverSocket = serverSocket; this.serverSocket = serverSocket;
} }
@Override @Override
public void run() { public BluetoothSocket call() throws IOException {
try { BluetoothSocket s = serverSocket.accept();
BluetoothSocket s = serverSocket.accept(); LOG.info("Incoming connection");
LOG.info("Incoming connection"); return s;
if (!socketLatch.set(s)) { }
LOG.info("Closing redundant connection"); }
s.close();
} private static class ReadableTask implements Callable<BluetoothSocket> {
} catch (IOException e) {
// This is expected when the socket is closed private final Callable<BluetoothSocket> connectionTask;
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
private ReadableTask(Callable<BluetoothSocket> connectionTask) {
this.connectionTask = connectionTask;
}
@Override
public BluetoothSocket call() throws Exception {
BluetoothSocket s = connectionTask.call();
InputStream in = s.getInputStream();
while (in.available() == 0) {
LOG.info("Waiting for data");
Thread.sleep(1000);
} }
LOG.info("Data available");
return s;
} }
} }
} }

View File

@@ -9,8 +9,6 @@ import org.briarproject.api.plugins.BackoffFactory;
import org.briarproject.api.plugins.duplex.DuplexPlugin; import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback; import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
import org.briarproject.api.plugins.duplex.DuplexPluginFactory; import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
import org.briarproject.api.system.Clock;
import org.briarproject.system.SystemClock;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@@ -27,7 +25,6 @@ public class DroidtoothPluginFactory implements DuplexPluginFactory {
private final Context appContext; private final Context appContext;
private final SecureRandom secureRandom; private final SecureRandom secureRandom;
private final BackoffFactory backoffFactory; private final BackoffFactory backoffFactory;
private final Clock clock;
public DroidtoothPluginFactory(Executor ioExecutor, public DroidtoothPluginFactory(Executor ioExecutor,
AndroidExecutor androidExecutor, Context appContext, AndroidExecutor androidExecutor, Context appContext,
@@ -37,7 +34,6 @@ public class DroidtoothPluginFactory implements DuplexPluginFactory {
this.appContext = appContext; this.appContext = appContext;
this.secureRandom = secureRandom; this.secureRandom = secureRandom;
this.backoffFactory = backoffFactory; this.backoffFactory = backoffFactory;
clock = new SystemClock();
} }
public TransportId getId() { public TransportId getId() {
@@ -48,6 +44,6 @@ public class DroidtoothPluginFactory implements DuplexPluginFactory {
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL, Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE); MAX_POLLING_INTERVAL, BACKOFF_BASE);
return new DroidtoothPlugin(ioExecutor, androidExecutor, appContext, return new DroidtoothPlugin(ioExecutor, androidExecutor, appContext,
secureRandom, clock, backoff, callback, MAX_LATENCY); secureRandom, backoff, callback, MAX_LATENCY);
} }
} }

View File

@@ -565,7 +565,7 @@ class TorPlugin implements DuplexPlugin, EventHandler,
} }
public DuplexTransportConnection createInvitationConnection(PseudoRandom r, public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout) { long timeout, boolean alice) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }

View File

@@ -23,5 +23,5 @@ public interface DuplexPlugin extends Plugin {
* time. * time.
*/ */
DuplexTransportConnection createInvitationConnection(PseudoRandom r, DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout); long timeout, boolean alice);
} }

View File

@@ -51,7 +51,7 @@ class AliceConnector extends Connector {
@Override @Override
public void run() { public void run() {
// Create an incoming or outgoing connection // Create an incoming or outgoing connection
DuplexTransportConnection conn = createInvitationConnection(); DuplexTransportConnection conn = createInvitationConnection(true);
if (conn == null) return; if (conn == null) return;
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " connected"); if (LOG.isLoggable(INFO)) LOG.info(pluginName + " connected");
// Don't proceed with more than one connection // Don't proceed with more than one connection

View File

@@ -51,7 +51,7 @@ class BobConnector extends Connector {
@Override @Override
public void run() { public void run() {
// Create an incoming or outgoing connection // Create an incoming or outgoing connection
DuplexTransportConnection conn = createInvitationConnection(); DuplexTransportConnection conn = createInvitationConnection(false);
if (conn == null) return; if (conn == null) return;
if (LOG.isLoggable(INFO)) LOG.info(pluginName + " connected"); if (LOG.isLoggable(INFO)) LOG.info(pluginName + " connected");
// Carry out the key agreement protocol // Carry out the key agreement protocol

View File

@@ -93,10 +93,12 @@ abstract class Connector extends Thread {
messageDigest = crypto.getMessageDigest(); messageDigest = crypto.getMessageDigest();
} }
protected DuplexTransportConnection createInvitationConnection() { protected DuplexTransportConnection createInvitationConnection(
boolean alice) {
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info(pluginName + " creating invitation connection"); LOG.info(pluginName + " creating invitation connection");
return plugin.createInvitationConnection(random, CONNECTION_TIMEOUT); return plugin.createInvitationConnection(random, CONNECTION_TIMEOUT,
alice);
} }
protected void sendPublicKeyHash(BdfWriter w) throws IOException { protected void sendPublicKeyHash(BdfWriter w) throws IOException {

View File

@@ -246,7 +246,7 @@ abstract class TcpPlugin implements DuplexPlugin {
} }
public DuplexTransportConnection createInvitationConnection(PseudoRandom r, public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout) { long timeout, boolean alice) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }

View File

@@ -1,30 +0,0 @@
package org.briarproject.util;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
public class LatchedReference<T> {
private final CountDownLatch latch = new CountDownLatch(1);
private final AtomicReference<T> reference = new AtomicReference<T>();
public boolean isSet() {
return reference.get() != null;
}
public boolean set(T t) {
if (t == null) throw new IllegalArgumentException();
if (reference.compareAndSet(null, t)) {
latch.countDown();
return true;
}
return false;
}
public T waitForReference(long timeout) throws InterruptedException {
latch.await(timeout, MILLISECONDS);
return reference.get();
}
}

View File

@@ -8,18 +8,24 @@ import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback; import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
import org.briarproject.api.plugins.duplex.DuplexTransportConnection; import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
import org.briarproject.api.properties.TransportProperties; import org.briarproject.api.properties.TransportProperties;
import org.briarproject.api.system.Clock;
import org.briarproject.util.LatchedReference;
import org.briarproject.util.OsUtils; import org.briarproject.util.OsUtils;
import org.briarproject.util.StringUtils; import org.briarproject.util.StringUtils;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore; import java.util.concurrent.Semaphore;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -30,6 +36,7 @@ import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection; import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier; import javax.microedition.io.StreamConnectionNotifier;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static javax.bluetooth.DiscoveryAgent.GIAC; import static javax.bluetooth.DiscoveryAgent.GIAC;
@@ -45,7 +52,6 @@ class BluetoothPlugin implements DuplexPlugin {
private final Executor ioExecutor; private final Executor ioExecutor;
private final SecureRandom secureRandom; private final SecureRandom secureRandom;
private final Clock clock;
private final Backoff backoff; private final Backoff backoff;
private final DuplexPluginCallback callback; private final DuplexPluginCallback callback;
private final int maxLatency; private final int maxLatency;
@@ -55,11 +61,10 @@ class BluetoothPlugin implements DuplexPlugin {
private volatile StreamConnectionNotifier socket = null; private volatile StreamConnectionNotifier socket = null;
private volatile LocalDevice localDevice = null; private volatile LocalDevice localDevice = null;
BluetoothPlugin(Executor ioExecutor, SecureRandom secureRandom, Clock clock, BluetoothPlugin(Executor ioExecutor, SecureRandom secureRandom,
Backoff backoff, DuplexPluginCallback callback, int maxLatency) { Backoff backoff, DuplexPluginCallback callback, int maxLatency) {
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
this.secureRandom = secureRandom; this.secureRandom = secureRandom;
this.clock = clock;
this.backoff = backoff; this.backoff = backoff;
this.callback = callback; this.callback = callback;
this.maxLatency = maxLatency; this.maxLatency = maxLatency;
@@ -246,7 +251,7 @@ class BluetoothPlugin implements DuplexPlugin {
} }
public DuplexTransportConnection createInvitationConnection(PseudoRandom r, public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout) { long timeout, boolean alice) {
if (!running) return null; if (!running) return null;
// Use the invitation codes to generate the UUID // Use the invitation codes to generate the UUID
byte[] b = r.nextBytes(UUID_BYTES); byte[] b = r.nextBytes(UUID_BYTES);
@@ -266,23 +271,68 @@ class BluetoothPlugin implements DuplexPlugin {
tryToClose(ss); tryToClose(ss);
return null; return null;
} }
// Start the background threads // Create the background tasks
LatchedReference<StreamConnection> socketLatch = CompletionService<StreamConnection> complete =
new LatchedReference<StreamConnection>(); new ExecutorCompletionService<StreamConnection>(ioExecutor);
new DiscoveryThread(socketLatch, uuid, timeout).start(); List<Future<StreamConnection>> futures =
new BluetoothListenerThread(socketLatch, ss).start(); new ArrayList<Future<StreamConnection>>();
// Wait for an incoming or outgoing connection if (alice) {
try { // Return the first connected socket
StreamConnection s = socketLatch.waitForReference(timeout); futures.add(complete.submit(new ListeningTask(ss)));
if (s != null) return new BluetoothTransportConnection(this, s); futures.add(complete.submit(new DiscoveryTask(uuid)));
} catch (InterruptedException e) { } else {
LOG.warning("Interrupted while exchanging invitations"); // Return the first socket with readable data
Thread.currentThread().interrupt(); futures.add(complete.submit(new ReadableTask(
} finally { new ListeningTask(ss))));
// Closing the socket will terminate the listener thread futures.add(complete.submit(new ReadableTask(
tryToClose(ss); new DiscoveryTask(uuid))));
} }
return null; StreamConnection chosen = null;
try {
Future<StreamConnection> f = complete.poll(timeout, MILLISECONDS);
if (f == null) return null; // No task completed within the timeout
chosen = f.get();
return new BluetoothTransportConnection(this, chosen);
} catch (InterruptedException e) {
LOG.info("Interrupted while exchanging invitations");
Thread.currentThread().interrupt();
return null;
} catch (ExecutionException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null;
} finally {
// Closing the socket will terminate the listener task
tryToClose(ss);
closeSockets(futures, chosen);
}
}
private void closeSockets(final List<Future<StreamConnection>> futures,
final StreamConnection chosen) {
ioExecutor.execute(new Runnable() {
public void run() {
for (Future<StreamConnection> f : futures) {
try {
if (f.cancel(true)) {
LOG.info("Cancelled task");
} else {
StreamConnection s = f.get();
if (s != null && s != chosen) {
LOG.info("Closing unwanted socket");
s.close();
}
}
} catch (InterruptedException e) {
LOG.info("Interrupted while closing sockets");
return;
} catch (ExecutionException e) {
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
} catch (IOException e) {
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
}
}
}
});
} }
private void makeDeviceDiscoverable() { private void makeDeviceDiscoverable() {
@@ -294,93 +344,74 @@ class BluetoothPlugin implements DuplexPlugin {
} }
} }
private class DiscoveryThread extends Thread { private class DiscoveryTask implements Callable<StreamConnection> {
private final LatchedReference<StreamConnection> socketLatch;
private final String uuid; private final String uuid;
private final long timeout;
private DiscoveryThread(LatchedReference<StreamConnection> socketLatch, private DiscoveryTask(String uuid) {
String uuid, long timeout) {
this.socketLatch = socketLatch;
this.uuid = uuid; this.uuid = uuid;
this.timeout = timeout;
} }
@Override @Override
public void run() { public StreamConnection call() throws Exception {
// Repeat discovery until we connect or get interrupted
DiscoveryAgent discoveryAgent = localDevice.getDiscoveryAgent(); DiscoveryAgent discoveryAgent = localDevice.getDiscoveryAgent();
long now = clock.currentTimeMillis(); while (true) {
long end = now + timeout; if (!discoverySemaphore.tryAcquire())
while (now < end && running && !socketLatch.isSet()) { throw new Exception("Discovery is already in progress");
if (!discoverySemaphore.tryAcquire()) {
LOG.info("Another device discovery is in progress");
return;
}
try { try {
InvitationListener listener = InvitationListener listener =
new InvitationListener(discoveryAgent, uuid); new InvitationListener(discoveryAgent, uuid);
discoveryAgent.startInquiry(GIAC, listener); discoveryAgent.startInquiry(GIAC, listener);
String url = listener.waitForUrl(); String url = listener.waitForUrl();
if (url == null) continue; if (url != null) {
StreamConnection s = connect(url); StreamConnection s = connect(url);
if (s == null) continue; if (s != null) {
LOG.info("Outgoing connection"); LOG.info("Outgoing connection");
if (!socketLatch.set(s)) { return s;
LOG.info("Closing redundant connection"); }
tryToClose(s);
} }
return;
} catch (BluetoothStateException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
return;
} catch (InterruptedException e) {
LOG.warning("Interrupted while waiting for URL");
Thread.currentThread().interrupt();
return;
} finally { } finally {
discoverySemaphore.release(); discoverySemaphore.release();
} }
} }
} }
private void tryToClose(StreamConnection s) {
try {
if (s != null) s.close();
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
} }
private static class BluetoothListenerThread extends Thread { private static class ListeningTask implements Callable<StreamConnection> {
private final LatchedReference<StreamConnection> socketLatch;
private final StreamConnectionNotifier serverSocket; private final StreamConnectionNotifier serverSocket;
private BluetoothListenerThread( private ListeningTask(StreamConnectionNotifier serverSocket) {
LatchedReference<StreamConnection> socketLatch,
StreamConnectionNotifier serverSocket) {
this.socketLatch = socketLatch;
this.serverSocket = serverSocket; this.serverSocket = serverSocket;
} }
@Override @Override
public void run() { public StreamConnection call() throws Exception {
LOG.info("Listening for invitation connections"); StreamConnection s = serverSocket.acceptAndOpen();
// Listen until a connection is received or the socket is closed LOG.info("Incoming connection");
try { return s;
StreamConnection s = serverSocket.acceptAndOpen(); }
LOG.info("Incoming connection"); }
if (!socketLatch.set(s)) {
LOG.info("Closing redundant connection"); private static class ReadableTask implements Callable<StreamConnection> {
s.close();
} private final Callable<StreamConnection> connectionTask;
} catch (IOException e) {
// This is expected when the socket is closed private ReadableTask(Callable<StreamConnection> connectionTask) {
if (LOG.isLoggable(INFO)) LOG.info(e.toString()); this.connectionTask = connectionTask;
}
@Override
public StreamConnection call() throws Exception {
StreamConnection s = connectionTask.call();
InputStream in = s.openInputStream();
while (in.available() == 0) {
LOG.info("Waiting for data");
Thread.sleep(1000);
} }
LOG.info("Data available");
return s;
} }
} }
} }

View File

@@ -6,8 +6,6 @@ import org.briarproject.api.plugins.BackoffFactory;
import org.briarproject.api.plugins.duplex.DuplexPlugin; import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback; import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
import org.briarproject.api.plugins.duplex.DuplexPluginFactory; import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
import org.briarproject.api.system.Clock;
import org.briarproject.system.SystemClock;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@@ -22,14 +20,12 @@ public class BluetoothPluginFactory implements DuplexPluginFactory {
private final Executor ioExecutor; private final Executor ioExecutor;
private final SecureRandom secureRandom; private final SecureRandom secureRandom;
private final BackoffFactory backoffFactory; private final BackoffFactory backoffFactory;
private final Clock clock;
public BluetoothPluginFactory(Executor ioExecutor, public BluetoothPluginFactory(Executor ioExecutor,
SecureRandom secureRandom, BackoffFactory backoffFactory) { SecureRandom secureRandom, BackoffFactory backoffFactory) {
this.ioExecutor = ioExecutor; this.ioExecutor = ioExecutor;
this.secureRandom = secureRandom; this.secureRandom = secureRandom;
this.backoffFactory = backoffFactory; this.backoffFactory = backoffFactory;
clock = new SystemClock();
} }
public TransportId getId() { public TransportId getId() {
@@ -39,7 +35,7 @@ public class BluetoothPluginFactory implements DuplexPluginFactory {
public DuplexPlugin createPlugin(DuplexPluginCallback callback) { public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL, Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
MAX_POLLING_INTERVAL, BACKOFF_BASE); MAX_POLLING_INTERVAL, BACKOFF_BASE);
return new BluetoothPlugin(ioExecutor, secureRandom, clock, backoff, return new BluetoothPlugin(ioExecutor, secureRandom, backoff, callback,
callback, MAX_LATENCY); MAX_LATENCY);
} }
} }

View File

@@ -154,7 +154,7 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
} }
public DuplexTransportConnection createInvitationConnection(PseudoRandom r, public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
long timeout) { long timeout, boolean alice) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }

View File

@@ -1,111 +0,0 @@
package org.briarproject.plugins;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.crypto.PseudoRandom;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
import org.briarproject.api.properties.TransportProperties;
import org.briarproject.api.settings.Settings;
import java.io.IOException;
import java.util.Map;
import static org.briarproject.api.invitation.InvitationConstants.CONNECTION_TIMEOUT;
public abstract class DuplexClientTest extends DuplexTest {
protected ClientCallback callback = null;
protected void run() throws IOException {
assert plugin != null;
// Start the plugin
System.out.println("Starting plugin");
if (!plugin.start()) {
System.out.println("Plugin failed to start");
return;
}
try {
// Try to connect to the server
System.out.println("Creating connection");
DuplexTransportConnection d = plugin.createConnection(contactId);
if (d == null) {
System.out.println("Connection failed");
return;
} else {
System.out.println("Connection created");
receiveChallengeSendResponse(d);
}
if (!plugin.supportsInvitations()) {
System.out.println("Skipping invitation test");
return;
}
// Try to create an invitation connection
System.out.println("Creating invitation connection");
PseudoRandom r = getPseudoRandom(123);
d = plugin.createInvitationConnection(r, CONNECTION_TIMEOUT);
if (d == null) {
System.out.println("Connection failed");
} else {
System.out.println("Connection created");
sendChallengeReceiveResponse(d);
}
} finally {
// Stop the plugin
System.out.println("Stopping plugin");
plugin.stop();
}
}
protected static class ClientCallback implements DuplexPluginCallback {
private Settings settings = null;
private TransportProperties local = null;
private Map<ContactId, TransportProperties> remote = null;
public ClientCallback(Settings settings, TransportProperties local,
Map<ContactId, TransportProperties> remote) {
this.settings = settings;
this.local = local;
this.remote = remote;
}
public Settings getSettings() {
return settings;
}
public TransportProperties getLocalProperties() {
return local;
}
public Map<ContactId, TransportProperties> getRemoteProperties() {
return remote;
}
public void mergeSettings(Settings s) {
settings = s;
}
public void mergeLocalProperties(TransportProperties p) {
local = p;
}
public int showChoice(String[] options, String... message) {
return -1;
}
public boolean showConfirmationMessage(String... message) {
return false;
}
public void showMessage(String... message) {}
public void incomingConnectionCreated(DuplexTransportConnection d) {}
public void outgoingConnectionCreated(ContactId contactId,
DuplexTransportConnection d) {}
public void transportEnabled() {}
public void transportDisabled() {}
}
}

View File

@@ -1,114 +0,0 @@
package org.briarproject.plugins;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
import org.briarproject.api.properties.TransportProperties;
import org.briarproject.api.settings.Settings;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.briarproject.api.invitation.InvitationConstants.CONNECTION_TIMEOUT;
public abstract class DuplexServerTest extends DuplexTest {
protected ServerCallback callback = null;
protected void run() throws Exception {
assert callback != null;
assert plugin != null;
// Start the plugin
System.out.println("Starting plugin");
if (!plugin.start()) {
System.out.println("Plugin failed to start");
return;
}
try {
// Wait for a connection
System.out.println("Waiting for connection");
if (!callback.latch.await(120, SECONDS)) {
System.out.println("No connection received");
return;
}
if (!plugin.supportsInvitations()) {
System.out.println("Skipping invitation test");
return;
}
// Try to create an invitation connection
System.out.println("Creating invitation connection");
DuplexTransportConnection d = plugin.createInvitationConnection(
getPseudoRandom(123), CONNECTION_TIMEOUT);
if (d == null) {
System.out.println("Connection failed");
} else {
System.out.println("Connection created");
receiveChallengeSendResponse(d);
}
} finally {
// Stop the plugin
System.out.println("Stopping plugin");
plugin.stop();
}
}
protected class ServerCallback implements DuplexPluginCallback {
private final CountDownLatch latch = new CountDownLatch(1);
private Settings settings;
private TransportProperties local;
private Map<ContactId, TransportProperties> remote;
public ServerCallback(Settings settings, TransportProperties local,
Map<ContactId, TransportProperties> remote) {
this.settings = settings;
this.local = local;
this.remote = remote;
}
public Settings getSettings() {
return settings;
}
public TransportProperties getLocalProperties() {
return local;
}
public Map<ContactId, TransportProperties> getRemoteProperties() {
return remote;
}
public void mergeSettings(Settings s) {
settings = s;
}
public void mergeLocalProperties(TransportProperties p) {
local = p;
}
public int showChoice(String[] options, String... message) {
return -1;
}
public boolean showConfirmationMessage(String... message) {
return false;
}
public void showMessage(String... message) {}
public void incomingConnectionCreated(DuplexTransportConnection d) {
System.out.println("Connection received");
sendChallengeReceiveResponse(d);
latch.countDown();
}
public void outgoingConnectionCreated(ContactId c,
DuplexTransportConnection d) {}
public void transportEnabled() {}
public void transportDisabled() {}
}
}

View File

@@ -1,102 +0,0 @@
package org.briarproject.plugins;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.crypto.PseudoRandom;
import org.briarproject.api.plugins.TransportConnectionReader;
import org.briarproject.api.plugins.TransportConnectionWriter;
import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Random;
import java.util.Scanner;
abstract class DuplexTest {
protected static final String CHALLENGE = "Carrots!";
protected static final String RESPONSE = "Potatoes!";
protected final ContactId contactId = new ContactId(234);
protected DuplexPlugin plugin = null;
protected void sendChallengeReceiveResponse(DuplexTransportConnection d) {
assert plugin != null;
TransportConnectionReader r = d.getReader();
TransportConnectionWriter w = d.getWriter();
try {
PrintStream out = new PrintStream(w.getOutputStream());
out.println(CHALLENGE);
out.flush();
System.out.println("Sent challenge: " + CHALLENGE);
Scanner in = new Scanner(r.getInputStream());
if (in.hasNextLine()) {
String response = in.nextLine();
System.out.println("Received response: " + response);
if (RESPONSE.equals(response)) {
System.out.println("Correct response");
} else {
System.out.println("Incorrect response");
}
} else {
System.out.println("No response");
}
r.dispose(false, true);
w.dispose(false);
} catch (IOException e) {
e.printStackTrace();
try {
r.dispose(true, true);
w.dispose(true);
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
protected void receiveChallengeSendResponse(DuplexTransportConnection d) {
assert plugin != null;
TransportConnectionReader r = d.getReader();
TransportConnectionWriter w = d.getWriter();
try {
Scanner in = new Scanner(r.getInputStream());
if (in.hasNextLine()) {
String challenge = in.nextLine();
System.out.println("Received challenge: " + challenge);
if (CHALLENGE.equals(challenge)) {
PrintStream out = new PrintStream(w.getOutputStream());
out.println(RESPONSE);
out.flush();
System.out.println("Sent response: " + RESPONSE);
} else {
System.out.println("Incorrect challenge");
}
} else {
System.out.println("No challenge");
}
r.dispose(false, true);
w.dispose(false);
} catch (IOException e) {
e.printStackTrace();
try {
r.dispose(true, true);
w.dispose(true);
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
protected PseudoRandom getPseudoRandom(int seed) {
final Random random = new Random(seed);
return new PseudoRandom() {
public byte[] nextBytes(int bytes) {
byte[] b = new byte[bytes];
random.nextBytes(b);
return b;
}
};
}
}