From 4431e502f7a6a328ee0c2b8a66f1aa81ed023a43 Mon Sep 17 00:00:00 2001 From: akwizgran <michael@briarproject.org> Date: Thu, 6 Jun 2013 15:17:35 +0100 Subject: [PATCH] Symmetric invitation protocol for the Droidtooth plugin. --- .../plugins/droidtooth/DroidtoothPlugin.java | 242 ++++++++++++------ .../droidtooth/DroidtoothPluginFactory.java | 6 +- 2 files changed, 165 insertions(+), 83 deletions(-) diff --git a/briar-core/src/net/sf/briar/plugins/droidtooth/DroidtoothPlugin.java b/briar-core/src/net/sf/briar/plugins/droidtooth/DroidtoothPlugin.java index 042d672924..b2d6bbab78 100644 --- a/briar-core/src/net/sf/briar/plugins/droidtooth/DroidtoothPlugin.java +++ b/briar-core/src/net/sf/briar/plugins/droidtooth/DroidtoothPlugin.java @@ -10,10 +10,11 @@ import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import java.io.IOException; -import java.net.SocketTimeoutException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; @@ -21,12 +22,15 @@ import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; import net.sf.briar.api.ContactId; import net.sf.briar.api.TransportId; import net.sf.briar.api.TransportProperties; import net.sf.briar.api.android.AndroidExecutor; +import net.sf.briar.api.clock.Clock; import net.sf.briar.api.crypto.PseudoRandom; import net.sf.briar.api.plugins.duplex.DuplexPlugin; import net.sf.briar.api.plugins.duplex.DuplexPluginCallback; @@ -61,6 +65,7 @@ class DroidtoothPlugin implements DuplexPlugin { private final AndroidExecutor androidExecutor; private final Context appContext; private final SecureRandom secureRandom; + private final Clock clock; private final DuplexPluginCallback callback; private final int maxFrameLength; private final long maxLatency, pollingInterval; @@ -72,13 +77,14 @@ class DroidtoothPlugin implements DuplexPlugin { private volatile BluetoothAdapter adapter = null; DroidtoothPlugin(Executor pluginExecutor, AndroidExecutor androidExecutor, - Context appContext, SecureRandom secureRandom, + Context appContext, SecureRandom secureRandom, Clock clock, DuplexPluginCallback callback, int maxFrameLength, long maxLatency, long pollingInterval) { this.pluginExecutor = pluginExecutor; this.androidExecutor = androidExecutor; this.appContext = appContext; this.secureRandom = secureRandom; + this.clock = clock; this.callback = callback; this.maxFrameLength = maxFrameLength; this.maxLatency = maxLatency; @@ -271,15 +277,18 @@ class DroidtoothPlugin implements DuplexPlugin { pluginExecutor.execute(new Runnable() { public void run() { if(!running) return; - DuplexTransportConnection conn = connect(address, uuid); - if(conn != null) - callback.outgoingConnectionCreated(c, conn); + BluetoothSocket s = connect(address, uuid); + if(s != null) { + callback.outgoingConnectionCreated(c, + new DroidtoothTransportConnection( + DroidtoothPlugin.this, s)); + } } }); } } - private DuplexTransportConnection connect(String address, String uuid) { + private BluetoothSocket connect(String address, String uuid) { // Validate the address if(!BluetoothAdapter.checkBluetoothAddress(address)) { if(LOG.isLoggable(WARNING)) @@ -302,7 +311,7 @@ class DroidtoothPlugin implements DuplexPlugin { if(LOG.isLoggable(INFO)) LOG.info("Connecting to " + address); s.connect(); if(LOG.isLoggable(INFO)) LOG.info("Connected to " + address); - return new DroidtoothTransportConnection(this, s); + return s; } catch(IOException e) { if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); tryToClose(s); @@ -326,7 +335,9 @@ class DroidtoothPlugin implements DuplexPlugin { if(StringUtils.isNullOrEmpty(address)) return null; String uuid = p.get("uuid"); if(StringUtils.isNullOrEmpty(uuid)) return null; - return connect(address, uuid); + BluetoothSocket s = connect(address, uuid); + if(s == null) return null; + return new DroidtoothTransportConnection(this, s); } public boolean supportsInvitations() { @@ -336,36 +347,11 @@ class DroidtoothPlugin implements DuplexPlugin { public DuplexTransportConnection sendInvitation(PseudoRandom r, long timeout) { if(!running) return null; - // Use the same pseudo-random UUID as the contact - byte[] b = r.nextBytes(UUID_BYTES); - String uuid = UUID.nameUUIDFromBytes(b).toString(); - if(LOG.isLoggable(INFO)) LOG.info("Sending invitation, UUID " + uuid); - // Register to receive Bluetooth discovery intents - IntentFilter filter = new IntentFilter(); - filter.addAction(FOUND); - filter.addAction(DISCOVERY_FINISHED); - // Discover nearby devices and connect to any with the right UUID - DiscoveryReceiver receiver = new DiscoveryReceiver(uuid); - appContext.registerReceiver(receiver, filter); - adapter.startDiscovery(); - try { - return receiver.waitForConnection(timeout); - } catch(InterruptedException e) { - if(LOG.isLoggable(INFO)) - LOG.info("Interrupted while sending invitation"); - Thread.currentThread().interrupt(); - return null; - } - } - - public DuplexTransportConnection acceptInvitation(PseudoRandom r, - long timeout) { - if(!running) return null; - // Use the same pseudo-random UUID as the contact + // Use the invitation codes to generate the UUID byte[] b = r.nextBytes(UUID_BYTES); UUID uuid = UUID.nameUUIDFromBytes(b); - if(LOG.isLoggable(INFO)) LOG.info("Accepting invitation, UUID " + uuid); - // Bind a new server socket to accept the invitation connection + if(LOG.isLoggable(INFO)) LOG.info("Invitation UUID " + uuid); + // Bind a server socket for receiving invitation connections BluetoothServerSocket ss = null; try { ss = InsecureBluetooth.listen(adapter, "RFCOMM", uuid); @@ -374,23 +360,29 @@ class DroidtoothPlugin implements DuplexPlugin { tryToClose(ss); return null; } - // Return the first connection received by the socket, if any + // Start the background threads + SocketReceiver receiver = new SocketReceiver(); + new DiscoveryThread(receiver, uuid.toString(), timeout).start(); + new BluetoothListenerThread(receiver, ss).start(); + // Wait for an incoming or outgoing connection try { - BluetoothSocket s = ss.accept((int) timeout); - if(LOG.isLoggable(INFO)) { - String address = s.getRemoteDevice().getAddress(); - LOG.info("Incoming connection from " + address); - } - return new DroidtoothTransportConnection(this, s); - } catch(SocketTimeoutException e) { - if(LOG.isLoggable(INFO)) LOG.info("Invitation timed out"); - return null; - } catch(IOException e) { - if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); - return null; + BluetoothSocket s = receiver.waitForSocket(timeout); + if(s != null) return new DroidtoothTransportConnection(this, s); + } catch(InterruptedException e) { + if(LOG.isLoggable(INFO)) + LOG.info("Interrupted while exchanging invitations"); + Thread.currentThread().interrupt(); } finally { + // Closing the socket will terminate the listener thread tryToClose(ss); } + return null; + } + + public DuplexTransportConnection acceptInvitation(PseudoRandom r, + long timeout) { + // FIXME + return sendInvitation(r, timeout); } private static class BluetoothStateReceiver extends BroadcastReceiver { @@ -403,67 +395,153 @@ class DroidtoothPlugin implements DuplexPlugin { int state = intent.getIntExtra(EXTRA_STATE, 0); if(state == STATE_ON) { enabled = true; - finish(ctx); + ctx.unregisterReceiver(this); + finished.countDown(); } else if(state == STATE_OFF) { - finish(ctx); + ctx.unregisterReceiver(this); + finished.countDown(); } } - private void finish(Context ctx) { - ctx.unregisterReceiver(this); - finished.countDown(); - } - boolean waitForStateChange() throws InterruptedException { finished.await(); return enabled; } } - private class DiscoveryReceiver extends BroadcastReceiver { + private static class SocketReceiver { - private final CountDownLatch finished = new CountDownLatch(1); - private final Collection<String> addresses = new ArrayList<String>(); - private final String uuid; + private final CountDownLatch latch = new CountDownLatch(1); + private final AtomicReference<BluetoothSocket> socket = + new AtomicReference<BluetoothSocket>(); + + private boolean hasSocket() { + return socket.get() != null; + } + + private boolean setSocket(BluetoothSocket s) { + if(socket.compareAndSet(null, s)) { + latch.countDown(); + return true; + } + return false; + } + + private BluetoothSocket waitForSocket(long timeout) + throws InterruptedException { + latch.await(timeout, TimeUnit.MILLISECONDS); + return socket.get(); + } + } - private volatile DuplexTransportConnection connection = null; + private class DiscoveryThread extends Thread { + + private final SocketReceiver receiver; + private final String uuid; + private final long timeout; - private DiscoveryReceiver(String uuid) { + private DiscoveryThread(SocketReceiver receiver, String uuid, + long timeout) { + this.receiver = receiver; this.uuid = uuid; + this.timeout = timeout; + } + + @Override + public void run() { + long now = clock.currentTimeMillis(); + long end = now + timeout; + while(now < end && running && !receiver.hasSocket()) { + // Discover nearby devices + if(LOG.isLoggable(INFO)) LOG.info("Discovering nearby devices"); + List<String> addresses; + try { + addresses = discoverDevices(end - now); + } catch(InterruptedException e) { + if(LOG.isLoggable(INFO)) + LOG.info("Interrupted while discovering devices"); + return; + } + // Connect to any device with the right UUID + for(String address : addresses) { + now = clock.currentTimeMillis(); + if(now < end && running && !receiver.hasSocket()) { + BluetoothSocket s = connect(address, uuid); + if(s == null) continue; + if(LOG.isLoggable(INFO)) + LOG.info("Outgoing connection"); + if(!receiver.setSocket(s)) { + if(LOG.isLoggable(INFO)) + LOG.info("Closing redundant connection"); + tryToClose(s); + } + return; + } + } + } + } + + private List<String> discoverDevices(long timeout) + throws InterruptedException { + IntentFilter filter = new IntentFilter(); + filter.addAction(FOUND); + filter.addAction(DISCOVERY_FINISHED); + DiscoveryReceiver disco = new DiscoveryReceiver(); + appContext.registerReceiver(disco, filter); + adapter.startDiscovery(); + return disco.waitForAddresses(timeout); } + } + + private class DiscoveryReceiver extends BroadcastReceiver { + + private final CountDownLatch finished = new CountDownLatch(1); + private final List<String> addresses = new ArrayList<String>(); @Override public void onReceive(Context ctx, Intent intent) { String action = intent.getAction(); if(action.equals(DISCOVERY_FINISHED)) { ctx.unregisterReceiver(this); - connectToDiscoveredDevices(); + finished.countDown(); } else if(action.equals(FOUND)) { BluetoothDevice d = intent.getParcelableExtra(EXTRA_DEVICE); - String address = d.getAddress(); - addresses.add(address); - } - } - - private void connectToDiscoveredDevices() { - for(final String address : addresses) { - pluginExecutor.execute(new Runnable() { - public void run() { - if(!running) return; - DuplexTransportConnection conn = connect(address, uuid); - if(conn != null) { - connection = conn; - finished.countDown(); - } - } - }); + addresses.add(d.getAddress()); } } - private DuplexTransportConnection waitForConnection(long timeout) + private List<String> waitForAddresses(long timeout) throws InterruptedException { finished.await(timeout, MILLISECONDS); - return connection; + return Collections.unmodifiableList(addresses); + } + } + + private class BluetoothListenerThread extends Thread { + + private final SocketReceiver receiver; + private final BluetoothServerSocket serverSocket; + + private BluetoothListenerThread(SocketReceiver receiver, + BluetoothServerSocket serverSocket) { + this.receiver = receiver; + this.serverSocket = serverSocket; + } + + @Override + public void run() { + try { + BluetoothSocket s = serverSocket.accept(); + if(LOG.isLoggable(INFO)) LOG.info("Incoming connection"); + if(!receiver.setSocket(s)) { + if(LOG.isLoggable(INFO)) + LOG.info("Closing redundant connection"); + tryToClose(s); + } + } catch(IOException e) { + // This is expected when the socket is closed + if(LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e); + } } } } diff --git a/briar-core/src/net/sf/briar/plugins/droidtooth/DroidtoothPluginFactory.java b/briar-core/src/net/sf/briar/plugins/droidtooth/DroidtoothPluginFactory.java index ee6566ed21..6599601c31 100644 --- a/briar-core/src/net/sf/briar/plugins/droidtooth/DroidtoothPluginFactory.java +++ b/briar-core/src/net/sf/briar/plugins/droidtooth/DroidtoothPluginFactory.java @@ -5,6 +5,8 @@ import java.util.concurrent.Executor; import net.sf.briar.api.TransportId; import net.sf.briar.api.android.AndroidExecutor; +import net.sf.briar.api.clock.Clock; +import net.sf.briar.api.clock.SystemClock; import net.sf.briar.api.plugins.duplex.DuplexPlugin; import net.sf.briar.api.plugins.duplex.DuplexPluginCallback; import net.sf.briar.api.plugins.duplex.DuplexPluginFactory; @@ -20,6 +22,7 @@ public class DroidtoothPluginFactory implements DuplexPluginFactory { private final AndroidExecutor androidExecutor; private final Context appContext; private final SecureRandom secureRandom; + private final Clock clock; public DroidtoothPluginFactory(Executor pluginExecutor, AndroidExecutor androidExecutor, Context appContext, @@ -28,6 +31,7 @@ public class DroidtoothPluginFactory implements DuplexPluginFactory { this.androidExecutor = androidExecutor; this.appContext = appContext; this.secureRandom = secureRandom; + clock = new SystemClock(); } public TransportId getId() { @@ -36,7 +40,7 @@ public class DroidtoothPluginFactory implements DuplexPluginFactory { public DuplexPlugin createPlugin(DuplexPluginCallback callback) { return new DroidtoothPlugin(pluginExecutor, androidExecutor, appContext, - secureRandom, callback, MAX_FRAME_LENGTH, MAX_LATENCY, + secureRandom, clock, callback, MAX_FRAME_LENGTH, MAX_LATENCY, POLLING_INTERVAL); } } -- GitLab