mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-15 04:18:53 +01:00
Moved Android-specific plugin code into briar-android project.
This removes the other projects' dependency on the Android API.
This commit is contained in:
@@ -50,8 +50,8 @@ public class AndroidModule extends AbstractModule {
|
||||
|
||||
protected void configure() {
|
||||
bind(AndroidExecutor.class).to(AndroidExecutorImpl.class);
|
||||
bind(ReferenceManager.class).to(ReferenceManagerImpl.class).in(
|
||||
Singleton.class);
|
||||
bind(ReferenceManager.class).to(
|
||||
ReferenceManagerImpl.class).in(Singleton.class);
|
||||
}
|
||||
|
||||
@Provides @Singleton @DatabaseUiExecutor
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
package net.sf.briar.plugins.droidtooth;
|
||||
|
||||
import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
|
||||
import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
|
||||
import static android.bluetooth.BluetoothAdapter.STATE_OFF;
|
||||
import static android.bluetooth.BluetoothAdapter.STATE_ON;
|
||||
import static android.bluetooth.BluetoothDevice.EXTRA_DEVICE;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
import java.io.IOException;
|
||||
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;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executor;
|
||||
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;
|
||||
import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
|
||||
import net.sf.briar.util.LatchedReference;
|
||||
import net.sf.briar.util.StringUtils;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothServerSocket;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
|
||||
class DroidtoothPlugin implements DuplexPlugin {
|
||||
|
||||
// Share an ID with the J2SE Bluetooth plugin
|
||||
static final byte[] TRANSPORT_ID =
|
||||
StringUtils.fromHexString("d99c9313c04417dcf22fc60d12a187ea"
|
||||
+ "00a539fd260f08a13a0d8a900cde5e49"
|
||||
+ "1b4df2ffd42e40c408f2db7868f518aa");
|
||||
static final TransportId ID = new TransportId(TRANSPORT_ID);
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(DroidtoothPlugin.class.getName());
|
||||
private static final int UUID_BYTES = 16;
|
||||
private static final String FOUND = "android.bluetooth.device.action.FOUND";
|
||||
private static final String DISCOVERY_FINISHED =
|
||||
"android.bluetooth.adapter.action.DISCOVERY_FINISHED";
|
||||
|
||||
private final Executor pluginExecutor;
|
||||
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;
|
||||
|
||||
private volatile boolean running = false;
|
||||
private volatile boolean wasEnabled = false, isEnabled = false;
|
||||
|
||||
// Non-null if running has ever been true
|
||||
private volatile BluetoothAdapter adapter = null;
|
||||
|
||||
DroidtoothPlugin(Executor pluginExecutor, AndroidExecutor androidExecutor,
|
||||
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;
|
||||
this.pollingInterval = pollingInterval;
|
||||
}
|
||||
|
||||
public TransportId getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
// Share a name with the J2SE Bluetooth plugin
|
||||
return "BLUETOOTH_PLUGIN_NAME";
|
||||
}
|
||||
|
||||
public int getMaxFrameLength() {
|
||||
return maxFrameLength;
|
||||
}
|
||||
|
||||
public long getMaxLatency() {
|
||||
return maxLatency;
|
||||
}
|
||||
|
||||
public boolean start() throws IOException {
|
||||
// BluetoothAdapter.getDefaultAdapter() must be called on a thread
|
||||
// with a message queue, so submit it to the AndroidExecutor
|
||||
try {
|
||||
adapter = androidExecutor.call(new Callable<BluetoothAdapter>() {
|
||||
public BluetoothAdapter call() throws Exception {
|
||||
return BluetoothAdapter.getDefaultAdapter();
|
||||
}
|
||||
});
|
||||
} catch(InterruptedException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(ExecutionException e) {
|
||||
throw new IOException(e.toString());
|
||||
}
|
||||
if(adapter == null) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Bluetooth is not supported");
|
||||
return false;
|
||||
}
|
||||
running = true;
|
||||
wasEnabled = isEnabled = adapter.isEnabled();
|
||||
pluginExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
bind();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private void bind() {
|
||||
if(!running) return;
|
||||
if(!enableBluetooth()) return;
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Local address " + adapter.getAddress());
|
||||
// Advertise the Bluetooth address to contacts
|
||||
TransportProperties p = new TransportProperties();
|
||||
p.put("address", adapter.getAddress());
|
||||
callback.mergeLocalProperties(p);
|
||||
// Bind a server socket to accept connections from contacts
|
||||
BluetoothServerSocket ss = null;
|
||||
try {
|
||||
ss = InsecureBluetooth.listen(adapter, "RFCOMM", getUuid());
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
tryToClose(ss);
|
||||
return;
|
||||
}
|
||||
if(!running) {
|
||||
tryToClose(ss);
|
||||
return;
|
||||
}
|
||||
acceptContactConnections(ss);
|
||||
}
|
||||
|
||||
private boolean enableBluetooth() {
|
||||
isEnabled = adapter.isEnabled();
|
||||
if(isEnabled) return true;
|
||||
// Try to enable the adapter and wait for the result
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Enabling Bluetooth");
|
||||
IntentFilter filter = new IntentFilter(ACTION_STATE_CHANGED);
|
||||
BluetoothStateReceiver receiver = new BluetoothStateReceiver();
|
||||
appContext.registerReceiver(receiver, filter);
|
||||
try {
|
||||
if(adapter.enable()) {
|
||||
isEnabled = receiver.waitForStateChange();
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Enabled: " + isEnabled);
|
||||
return isEnabled;
|
||||
} else {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Could not enable adapter");
|
||||
return false;
|
||||
}
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Interrupted while enabling Bluetooth");
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private UUID getUuid() {
|
||||
String uuid = callback.getLocalProperties().get("uuid");
|
||||
if(uuid == null) {
|
||||
byte[] random = new byte[UUID_BYTES];
|
||||
secureRandom.nextBytes(random);
|
||||
uuid = UUID.nameUUIDFromBytes(random).toString();
|
||||
TransportProperties p = new TransportProperties();
|
||||
p.put("uuid", uuid);
|
||||
callback.mergeLocalProperties(p);
|
||||
}
|
||||
return UUID.fromString(uuid);
|
||||
}
|
||||
|
||||
private void tryToClose(BluetoothServerSocket ss) {
|
||||
try {
|
||||
if(ss != null) ss.close();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptContactConnections(BluetoothServerSocket ss) {
|
||||
while(true) {
|
||||
BluetoothSocket s;
|
||||
try {
|
||||
s = ss.accept();
|
||||
} catch(IOException e) {
|
||||
// This is expected when the socket is closed
|
||||
if(LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
|
||||
tryToClose(ss);
|
||||
return;
|
||||
}
|
||||
callback.incomingConnectionCreated(wrapSocket(s));
|
||||
if(!running) return;
|
||||
}
|
||||
}
|
||||
|
||||
private DuplexTransportConnection wrapSocket(BluetoothSocket s) {
|
||||
return new DroidtoothTransportConnection(this, s);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
running = false;
|
||||
// Disable Bluetooth if we enabled it at startup
|
||||
if(isEnabled && !wasEnabled) disableBluetooth();
|
||||
}
|
||||
|
||||
private void disableBluetooth() {
|
||||
isEnabled = adapter.isEnabled();
|
||||
if(!isEnabled) return;
|
||||
// Try to disable the adapter and wait for the result
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Disabling Bluetooth");
|
||||
IntentFilter filter = new IntentFilter(ACTION_STATE_CHANGED);
|
||||
BluetoothStateReceiver receiver = new BluetoothStateReceiver();
|
||||
appContext.registerReceiver(receiver, filter);
|
||||
try {
|
||||
if(adapter.disable()) {
|
||||
isEnabled = receiver.waitForStateChange();
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Enabled: " + isEnabled);
|
||||
} else {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Could not disable adapter");
|
||||
}
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Interrupted while disabling Bluetooth");
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean shouldPoll() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public long getPollingInterval() {
|
||||
return pollingInterval;
|
||||
}
|
||||
|
||||
public void poll(Collection<ContactId> connected) {
|
||||
if(!running) return;
|
||||
if(!enableBluetooth()) return;
|
||||
// Try to connect to known devices in parallel
|
||||
Map<ContactId, TransportProperties> remote =
|
||||
callback.getRemoteProperties();
|
||||
for(Entry<ContactId, TransportProperties> e : remote.entrySet()) {
|
||||
final ContactId c = e.getKey();
|
||||
if(connected.contains(c)) continue;
|
||||
final String address = e.getValue().get("address");
|
||||
if(StringUtils.isNullOrEmpty(address)) continue;
|
||||
final String uuid = e.getValue().get("uuid");
|
||||
if(StringUtils.isNullOrEmpty(uuid)) continue;
|
||||
pluginExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
if(!running) return;
|
||||
BluetoothSocket s = connect(address, uuid);
|
||||
if(s != null)
|
||||
callback.outgoingConnectionCreated(c, wrapSocket(s));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private BluetoothSocket connect(String address, String uuid) {
|
||||
// Validate the address
|
||||
if(!BluetoothAdapter.checkBluetoothAddress(address)) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Invalid address " + address);
|
||||
return null;
|
||||
}
|
||||
// Validate the UUID
|
||||
UUID u;
|
||||
try {
|
||||
u = UUID.fromString(uuid);
|
||||
} catch(IllegalArgumentException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.warning("Invalid UUID " + uuid);
|
||||
return null;
|
||||
}
|
||||
// Try to connect
|
||||
BluetoothDevice d = adapter.getRemoteDevice(address);
|
||||
BluetoothSocket s = null;
|
||||
try {
|
||||
s = InsecureBluetooth.createSocket(d, u);
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Connecting to " + address);
|
||||
s.connect();
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Connected to " + address);
|
||||
return s;
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
tryToClose(s);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void tryToClose(BluetoothSocket s) {
|
||||
try {
|
||||
if(s != null) s.close();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public DuplexTransportConnection createConnection(ContactId c) {
|
||||
if(!running) return null;
|
||||
TransportProperties p = callback.getRemoteProperties().get(c);
|
||||
if(p == null) return null;
|
||||
String address = p.get("address");
|
||||
if(StringUtils.isNullOrEmpty(address)) return null;
|
||||
String uuid = p.get("uuid");
|
||||
if(StringUtils.isNullOrEmpty(uuid)) return null;
|
||||
BluetoothSocket s = connect(address, uuid);
|
||||
if(s == null) return null;
|
||||
return new DroidtoothTransportConnection(this, s);
|
||||
}
|
||||
|
||||
public boolean supportsInvitations() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
|
||||
long timeout) {
|
||||
if(!running) return null;
|
||||
// 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("Invitation UUID " + uuid);
|
||||
// Bind a server socket for receiving invitation connections
|
||||
BluetoothServerSocket ss = null;
|
||||
try {
|
||||
ss = InsecureBluetooth.listen(adapter, "RFCOMM", uuid);
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
tryToClose(ss);
|
||||
return null;
|
||||
}
|
||||
// Start the background threads
|
||||
LatchedReference<BluetoothSocket> socketLatch =
|
||||
new LatchedReference<BluetoothSocket>();
|
||||
new DiscoveryThread(socketLatch, uuid.toString(), timeout).start();
|
||||
new BluetoothListenerThread(socketLatch, ss).start();
|
||||
// Wait for an incoming or outgoing connection
|
||||
try {
|
||||
BluetoothSocket s = socketLatch.waitForReference(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;
|
||||
}
|
||||
|
||||
private static class BluetoothStateReceiver extends BroadcastReceiver {
|
||||
|
||||
private final CountDownLatch finished = new CountDownLatch(1);
|
||||
|
||||
private volatile boolean enabled = false;
|
||||
|
||||
public void onReceive(Context ctx, Intent intent) {
|
||||
int state = intent.getIntExtra(EXTRA_STATE, 0);
|
||||
if(state == STATE_ON) {
|
||||
enabled = true;
|
||||
ctx.unregisterReceiver(this);
|
||||
finished.countDown();
|
||||
} else if(state == STATE_OFF) {
|
||||
ctx.unregisterReceiver(this);
|
||||
finished.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
boolean waitForStateChange() throws InterruptedException {
|
||||
finished.await();
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
|
||||
private class DiscoveryThread extends Thread {
|
||||
|
||||
private final LatchedReference<BluetoothSocket> socketLatch;
|
||||
private final String uuid;
|
||||
private final long timeout;
|
||||
|
||||
private DiscoveryThread(LatchedReference<BluetoothSocket> socketLatch,
|
||||
String uuid, long timeout) {
|
||||
this.socketLatch = socketLatch;
|
||||
this.uuid = uuid;
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
long now = clock.currentTimeMillis();
|
||||
long end = now + timeout;
|
||||
while(now < end && running && !socketLatch.isSet()) {
|
||||
// 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 && !socketLatch.isSet()) {
|
||||
BluetoothSocket s = connect(address, uuid);
|
||||
if(s == null) continue;
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Outgoing connection");
|
||||
if(!socketLatch.set(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 static 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);
|
||||
finished.countDown();
|
||||
} else if(action.equals(FOUND)) {
|
||||
BluetoothDevice d = intent.getParcelableExtra(EXTRA_DEVICE);
|
||||
addresses.add(d.getAddress());
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> waitForAddresses(long timeout)
|
||||
throws InterruptedException {
|
||||
finished.await(timeout, MILLISECONDS);
|
||||
return Collections.unmodifiableList(addresses);
|
||||
}
|
||||
}
|
||||
|
||||
private static class BluetoothListenerThread extends Thread {
|
||||
|
||||
private final LatchedReference<BluetoothSocket> socketLatch;
|
||||
private final BluetoothServerSocket serverSocket;
|
||||
|
||||
private BluetoothListenerThread(
|
||||
LatchedReference<BluetoothSocket> socketLatch,
|
||||
BluetoothServerSocket serverSocket) {
|
||||
this.socketLatch = socketLatch;
|
||||
this.serverSocket = serverSocket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
BluetoothSocket s = serverSocket.accept();
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Incoming connection");
|
||||
if(!socketLatch.set(s)) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Closing redundant connection");
|
||||
s.close();
|
||||
}
|
||||
} catch(IOException e) {
|
||||
// This is expected when the socket is closed
|
||||
if(LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package net.sf.briar.plugins.droidtooth;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
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;
|
||||
import android.content.Context;
|
||||
|
||||
public class DroidtoothPluginFactory implements DuplexPluginFactory {
|
||||
|
||||
private static final int MAX_FRAME_LENGTH = 1024;
|
||||
private static final long MAX_LATENCY = 60 * 1000; // 1 minute
|
||||
private static final long POLLING_INTERVAL = 3 * 60 * 1000; // 3 minutes
|
||||
|
||||
private final Executor pluginExecutor;
|
||||
private final AndroidExecutor androidExecutor;
|
||||
private final Context appContext;
|
||||
private final SecureRandom secureRandom;
|
||||
private final Clock clock;
|
||||
|
||||
public DroidtoothPluginFactory(Executor pluginExecutor,
|
||||
AndroidExecutor androidExecutor, Context appContext,
|
||||
SecureRandom secureRandom) {
|
||||
this.pluginExecutor = pluginExecutor;
|
||||
this.androidExecutor = androidExecutor;
|
||||
this.appContext = appContext;
|
||||
this.secureRandom = secureRandom;
|
||||
clock = new SystemClock();
|
||||
}
|
||||
|
||||
public TransportId getId() {
|
||||
return DroidtoothPlugin.ID;
|
||||
}
|
||||
|
||||
public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
|
||||
return new DroidtoothPlugin(pluginExecutor, androidExecutor, appContext,
|
||||
secureRandom, clock, callback, MAX_FRAME_LENGTH, MAX_LATENCY,
|
||||
POLLING_INTERVAL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package net.sf.briar.plugins.droidtooth;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import net.sf.briar.api.plugins.Plugin;
|
||||
import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
|
||||
class DroidtoothTransportConnection implements DuplexTransportConnection {
|
||||
|
||||
private final Plugin plugin;
|
||||
private final BluetoothSocket socket;
|
||||
|
||||
DroidtoothTransportConnection(Plugin plugin, BluetoothSocket socket) {
|
||||
this.plugin = plugin;
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
public int getMaxFrameLength() {
|
||||
return plugin.getMaxFrameLength();
|
||||
}
|
||||
|
||||
public long getMaxLatency() {
|
||||
return plugin.getMaxLatency();
|
||||
}
|
||||
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return socket.getInputStream();
|
||||
}
|
||||
|
||||
public OutputStream getOutputStream() throws IOException {
|
||||
return socket.getOutputStream();
|
||||
}
|
||||
|
||||
public boolean shouldFlush() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void dispose(boolean exception, boolean recognised)
|
||||
throws IOException {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package net.sf.briar.plugins.droidtooth;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothServerSocket;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.ParcelUuid;
|
||||
|
||||
// Based on http://stanford.edu/~tpurtell/InsecureBluetooth.java by T.J. Purtell
|
||||
class InsecureBluetooth {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(InsecureBluetooth.class.getName());
|
||||
|
||||
private static final int TYPE_RFCOMM = 1;
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
static BluetoothServerSocket listen(BluetoothAdapter adapter, String name,
|
||||
UUID uuid) throws IOException {
|
||||
if(Build.VERSION.SDK_INT >= 10) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Listening with new API");
|
||||
return adapter.listenUsingInsecureRfcommWithServiceRecord(name,
|
||||
uuid);
|
||||
}
|
||||
try {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Listening via reflection");
|
||||
// Find an available channel
|
||||
String className = BluetoothAdapter.class.getCanonicalName()
|
||||
+ ".RfcommChannelPicker";
|
||||
Class<?> channelPickerClass = null;
|
||||
Class<?>[] children = BluetoothAdapter.class.getDeclaredClasses();
|
||||
for(Class<?> c : children) {
|
||||
if(c.getCanonicalName().equals(className)) {
|
||||
channelPickerClass = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(channelPickerClass == null)
|
||||
throw new IOException("Can't find channel picker class");
|
||||
Constructor<?> constructor =
|
||||
channelPickerClass.getDeclaredConstructor(UUID.class);
|
||||
constructor.setAccessible(true);
|
||||
Object channelPicker = constructor.newInstance(uuid);
|
||||
Method nextChannel =
|
||||
channelPickerClass.getDeclaredMethod("nextChannel");
|
||||
nextChannel.setAccessible(true);
|
||||
int channel = (Integer) nextChannel.invoke(channelPicker);
|
||||
if(channel == -1) throw new IOException("No available channels");
|
||||
// Listen on the channel
|
||||
BluetoothServerSocket socket = listen(channel);
|
||||
// Add a service record
|
||||
Field f = BluetoothAdapter.class.getDeclaredField("mService");
|
||||
f.setAccessible(true);
|
||||
Object mService = f.get(adapter);
|
||||
Method addRfcommServiceRecord =
|
||||
mService.getClass().getDeclaredMethod(
|
||||
"addRfcommServiceRecord", String.class,
|
||||
ParcelUuid.class, int.class, IBinder.class);
|
||||
addRfcommServiceRecord.setAccessible(true);
|
||||
int handle = (Integer) addRfcommServiceRecord.invoke(mService, name,
|
||||
new ParcelUuid(uuid), channel, new Binder());
|
||||
if(handle == -1) {
|
||||
socket.close();
|
||||
throw new IOException("Can't register SDP record for " + name);
|
||||
}
|
||||
Field f1 = BluetoothAdapter.class.getDeclaredField("mHandler");
|
||||
f1.setAccessible(true);
|
||||
Object mHandler = f1.get(adapter);
|
||||
Method setCloseHandler = socket.getClass().getDeclaredMethod(
|
||||
"setCloseHandler", Handler.class, int.class);
|
||||
setCloseHandler.setAccessible(true);
|
||||
setCloseHandler.invoke(socket, mHandler, handle);
|
||||
return socket;
|
||||
} catch(NoSuchMethodException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(NoSuchFieldException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(IllegalAccessException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(InstantiationException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(InvocationTargetException e) {
|
||||
if(e.getCause() instanceof IOException) {
|
||||
throw (IOException) e.getCause();
|
||||
} else {
|
||||
throw new IOException(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BluetoothServerSocket listen(int port) throws IOException {
|
||||
try {
|
||||
Constructor<BluetoothServerSocket> constructor =
|
||||
BluetoothServerSocket.class.getDeclaredConstructor(
|
||||
int.class, boolean.class, boolean.class, int.class);
|
||||
constructor.setAccessible(true);
|
||||
BluetoothServerSocket socket = constructor.newInstance(TYPE_RFCOMM,
|
||||
false, false, port);
|
||||
Field f = BluetoothServerSocket.class.getDeclaredField("mSocket");
|
||||
f.setAccessible(true);
|
||||
Object mSocket = f.get(socket);
|
||||
Method bindListen =
|
||||
mSocket.getClass().getDeclaredMethod("bindListen");
|
||||
bindListen.setAccessible(true);
|
||||
int errno = (Integer) bindListen.invoke(mSocket);
|
||||
if(errno != 0) {
|
||||
socket.close();
|
||||
throw new IOException("Can't bind: errno " + errno);
|
||||
}
|
||||
return socket;
|
||||
} catch(NoSuchMethodException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(NoSuchFieldException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(IllegalAccessException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(InstantiationException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(InvocationTargetException e) {
|
||||
if(e.getCause() instanceof IOException) {
|
||||
throw (IOException) e.getCause();
|
||||
} else {
|
||||
throw new IOException(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
static BluetoothSocket createSocket(BluetoothDevice device, UUID uuid)
|
||||
throws IOException {
|
||||
if(Build.VERSION.SDK_INT >= 10) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Creating socket with new API");
|
||||
return device.createInsecureRfcommSocketToServiceRecord(uuid);
|
||||
}
|
||||
try {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Creating socket via reflection");
|
||||
Constructor<BluetoothSocket> constructor =
|
||||
BluetoothSocket.class.getDeclaredConstructor(int.class,
|
||||
int.class, boolean.class, boolean.class,
|
||||
BluetoothDevice.class, int.class, ParcelUuid.class);
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(TYPE_RFCOMM, -1, false, true, device,
|
||||
-1, new ParcelUuid(uuid));
|
||||
} catch(NoSuchMethodException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(IllegalAccessException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(InstantiationException e) {
|
||||
throw new IOException(e.toString());
|
||||
} catch(InvocationTargetException e) {
|
||||
if(e.getCause() instanceof IOException) {
|
||||
throw (IOException) e.getCause();
|
||||
} else {
|
||||
throw new IOException(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package net.sf.briar.plugins.tcp;
|
||||
|
||||
import static android.content.Context.WIFI_SERVICE;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import net.sf.briar.api.clock.Clock;
|
||||
import net.sf.briar.api.crypto.PseudoRandom;
|
||||
import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
|
||||
import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
|
||||
import android.content.Context;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.net.wifi.WifiManager.MulticastLock;
|
||||
|
||||
class DroidLanTcpPlugin extends LanTcpPlugin {
|
||||
|
||||
private final Context appContext;
|
||||
|
||||
DroidLanTcpPlugin(Executor pluginExecutor, Context appContext, Clock clock,
|
||||
DuplexPluginCallback callback, int maxFrameLength, long maxLatency,
|
||||
long pollingInterval) {
|
||||
super(pluginExecutor, clock, callback, maxFrameLength, maxLatency,
|
||||
pollingInterval);
|
||||
this.appContext = appContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
|
||||
long timeout) {
|
||||
WifiManager wifi =
|
||||
(WifiManager) appContext.getSystemService(WIFI_SERVICE);
|
||||
if(wifi == null || !wifi.isWifiEnabled()) return null;
|
||||
MulticastLock lock = wifi.createMulticastLock("invitation");
|
||||
lock.acquire();
|
||||
try {
|
||||
return super.createInvitationConnection(r, timeout);
|
||||
} finally {
|
||||
lock.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package net.sf.briar.plugins.tcp;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import net.sf.briar.api.TransportId;
|
||||
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;
|
||||
import android.content.Context;
|
||||
|
||||
public class DroidLanTcpPluginFactory implements DuplexPluginFactory {
|
||||
|
||||
private static final int MAX_FRAME_LENGTH = 1024;
|
||||
private static final long MAX_LATENCY = 60 * 1000; // 1 minute
|
||||
private static final long POLLING_INTERVAL = 60 * 1000; // 1 minute
|
||||
|
||||
private final Executor pluginExecutor;
|
||||
private final Context appContext;
|
||||
private final Clock clock;
|
||||
|
||||
public DroidLanTcpPluginFactory(Executor pluginExecutor,
|
||||
Context appContext) {
|
||||
this.pluginExecutor = pluginExecutor;
|
||||
this.appContext = appContext;
|
||||
clock = new SystemClock();
|
||||
}
|
||||
|
||||
public TransportId getId() {
|
||||
return LanTcpPlugin.ID;
|
||||
}
|
||||
|
||||
public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
|
||||
return new DroidLanTcpPlugin(pluginExecutor, appContext, clock,
|
||||
callback, MAX_FRAME_LENGTH, MAX_LATENCY, POLLING_INTERVAL);
|
||||
}
|
||||
}
|
||||
545
briar-android/src/net/sf/briar/plugins/tor/TorPlugin.java
Normal file
545
briar-android/src/net/sf/briar/plugins/tor/TorPlugin.java
Normal file
@@ -0,0 +1,545 @@
|
||||
package net.sf.briar.plugins.tor;
|
||||
|
||||
import static android.content.Context.MODE_PRIVATE;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import net.freehaven.tor.control.EventHandler;
|
||||
import net.freehaven.tor.control.TorControlConnection;
|
||||
import net.sf.briar.api.ContactId;
|
||||
import net.sf.briar.api.TransportConfig;
|
||||
import net.sf.briar.api.TransportId;
|
||||
import net.sf.briar.api.TransportProperties;
|
||||
import net.sf.briar.api.crypto.PseudoRandom;
|
||||
import net.sf.briar.api.lifecycle.ShutdownManager;
|
||||
import net.sf.briar.api.plugins.duplex.DuplexPlugin;
|
||||
import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
|
||||
import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
|
||||
import net.sf.briar.util.StringUtils;
|
||||
import socks.Socks5Proxy;
|
||||
import socks.SocksSocket;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.FileObserver;
|
||||
|
||||
class TorPlugin implements DuplexPlugin, EventHandler {
|
||||
|
||||
static final byte[] TRANSPORT_ID =
|
||||
StringUtils.fromHexString("fa866296495c73a52e6a82fd12db6f15"
|
||||
+ "47753b5e636bb8b24975780d7d2e3fc2"
|
||||
+ "d32a4c480c74de2dc6e3157a632a0287");
|
||||
static final TransportId ID = new TransportId(TRANSPORT_ID);
|
||||
|
||||
private static final int SOCKS_PORT = 59050, CONTROL_PORT = 59051;
|
||||
private static final int COOKIE_TIMEOUT = 3000; // Milliseconds
|
||||
private static final int HOSTNAME_TIMEOUT = 30 * 1000; // Milliseconds
|
||||
private static final Pattern ONION =
|
||||
Pattern.compile("[a-z2-7]{16}\\.onion");
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(TorPlugin.class.getName());
|
||||
|
||||
private final Executor pluginExecutor;
|
||||
private final Context appContext;
|
||||
private final ShutdownManager shutdownManager;
|
||||
private final DuplexPluginCallback callback;
|
||||
private final int maxFrameLength;
|
||||
private final long maxLatency, pollingInterval;
|
||||
private final File torDirectory, torFile, geoIpFile, configFile, doneFile;
|
||||
private final File cookieFile, pidFile, hostnameFile;
|
||||
|
||||
private volatile boolean running = false;
|
||||
private volatile Process tor = null;
|
||||
private volatile int pid = -1;
|
||||
private volatile ServerSocket socket = null;
|
||||
private volatile Socket controlSocket = null;
|
||||
private volatile TorControlConnection controlConnection = null;
|
||||
|
||||
TorPlugin(Executor pluginExecutor, Context appContext,
|
||||
ShutdownManager shutdownManager, DuplexPluginCallback callback,
|
||||
int maxFrameLength, long maxLatency, long pollingInterval) {
|
||||
this.pluginExecutor = pluginExecutor;
|
||||
this.appContext = appContext;
|
||||
this.shutdownManager = shutdownManager;
|
||||
this.callback = callback;
|
||||
this.maxFrameLength = maxFrameLength;
|
||||
this.maxLatency = maxLatency;
|
||||
this.pollingInterval = pollingInterval;
|
||||
torDirectory = appContext.getDir("tor", MODE_PRIVATE);
|
||||
torFile = new File(torDirectory, "tor");
|
||||
geoIpFile = new File(torDirectory, "geoip");
|
||||
configFile = new File(torDirectory, "torrc");
|
||||
doneFile = new File(torDirectory, "done");
|
||||
cookieFile = new File(torDirectory, ".tor/control_auth_cookie");
|
||||
pidFile = new File(torDirectory, ".tor/pid");
|
||||
hostnameFile = new File(torDirectory, "hostname");
|
||||
}
|
||||
|
||||
public TransportId getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return "TOR_PLUGIN_NAME";
|
||||
}
|
||||
|
||||
public int getMaxFrameLength() {
|
||||
return maxFrameLength;
|
||||
}
|
||||
|
||||
public long getMaxLatency() {
|
||||
return maxLatency;
|
||||
}
|
||||
|
||||
public boolean start() throws IOException {
|
||||
// Check that we have a Tor binary for this architecture
|
||||
if(!Build.CPU_ABI.startsWith("armeabi")) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("No Tor binary for this architecture");
|
||||
return false;
|
||||
}
|
||||
// Try to connect to an existing Tor process if there is one
|
||||
try {
|
||||
controlSocket = new Socket("127.0.0.1", CONTROL_PORT);
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Tor is already running");
|
||||
} catch(IOException e) {
|
||||
// Install the binary, GeoIP database and config file if necessary
|
||||
if(!isInstalled() && !install()) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Could not install Tor");
|
||||
return false;
|
||||
}
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Starting Tor");
|
||||
// Watch for the auth cookie file being created/updated
|
||||
cookieFile.getParentFile().mkdirs();
|
||||
cookieFile.createNewFile();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
FileObserver obs = new WriteObserver(cookieFile, latch);
|
||||
obs.startWatching();
|
||||
// Start a new Tor process
|
||||
String torPath = torFile.getAbsolutePath();
|
||||
String configPath = configFile.getAbsolutePath();
|
||||
String[] cmd = { torPath, "-f", configPath };
|
||||
String[] env = { "HOME=" + torDirectory.getAbsolutePath() };
|
||||
try {
|
||||
tor = Runtime.getRuntime().exec(cmd, env, torDirectory);
|
||||
} catch(SecurityException e1) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e1.toString(), e1);
|
||||
return false;
|
||||
}
|
||||
// Log the process's standard output until it detaches
|
||||
if(LOG.isLoggable(INFO)) {
|
||||
Scanner stdout = new Scanner(tor.getInputStream());
|
||||
while(stdout.hasNextLine()) LOG.info(stdout.nextLine());
|
||||
stdout.close();
|
||||
}
|
||||
try {
|
||||
// Wait for the process to detach or exit
|
||||
int exit = tor.waitFor();
|
||||
if(exit != 0) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Tor exited with value " + exit);
|
||||
return false;
|
||||
}
|
||||
// Wait for the auth cookie file to be created/updated
|
||||
if(!latch.await(COOKIE_TIMEOUT, MILLISECONDS)) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Auth cookie not created");
|
||||
listFiles(torDirectory);
|
||||
return false;
|
||||
}
|
||||
} catch(InterruptedException e1) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Interrupted while starting Tor");
|
||||
return false;
|
||||
}
|
||||
// Now we should be able to connect to the new process
|
||||
controlSocket = new Socket("127.0.0.1", CONTROL_PORT);
|
||||
}
|
||||
// Read the PID of the Tor process so we can kill it if necessary
|
||||
try {
|
||||
pid = Integer.parseInt(new String(read(pidFile), "UTF-8").trim());
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.warning("Could not read PID file");
|
||||
} catch(NumberFormatException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.warning("Could not parse PID file");
|
||||
}
|
||||
// Create a shutdown hook to ensure the Tor process is killed
|
||||
shutdownManager.addShutdownHook(new Runnable() {
|
||||
public void run() {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Killing Tor");
|
||||
if(tor != null) tor.destroy();
|
||||
if(pid != -1) android.os.Process.killProcess(pid);
|
||||
}
|
||||
});
|
||||
// Open a control connection and authenticate using the cookie file
|
||||
controlConnection = new TorControlConnection(controlSocket);
|
||||
controlConnection.authenticate(read(cookieFile));
|
||||
// Register to receive events from the Tor process
|
||||
controlConnection.setEventHandler(this);
|
||||
controlConnection.setEvents(Arrays.asList("NOTICE", "WARN", "ERR"));
|
||||
running = true;
|
||||
// Bind a server socket to receive incoming hidden service connections
|
||||
pluginExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
bind();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isInstalled() {
|
||||
return doneFile.exists();
|
||||
}
|
||||
|
||||
private boolean install() {
|
||||
InputStream in = null;
|
||||
OutputStream out = null;
|
||||
try {
|
||||
// Unzip the Tor binary to the filesystem
|
||||
in = getTorInputStream();
|
||||
out = new FileOutputStream(torFile);
|
||||
copy(in, out);
|
||||
// Unzip the GeoIP database to the filesystem
|
||||
in = getGeoIpInputStream();
|
||||
out = new FileOutputStream(geoIpFile);
|
||||
copy(in, out);
|
||||
// Copy the config file to the filesystem
|
||||
in = getConfigInputStream();
|
||||
out = new FileOutputStream(configFile);
|
||||
copy(in, out);
|
||||
// Make the Tor binary executable
|
||||
if(!setExecutable(torFile)) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Could not make Tor executable");
|
||||
return false;
|
||||
}
|
||||
// Create a file to indicate that installation succeeded
|
||||
doneFile.createNewFile();
|
||||
return true;
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
tryToClose(in);
|
||||
tryToClose(out);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream getTorInputStream() throws IOException {
|
||||
InputStream in = appContext.getResources().getAssets().open("tor");
|
||||
ZipInputStream zin = new ZipInputStream(in);
|
||||
if(zin.getNextEntry() == null) throw new IOException();
|
||||
return zin;
|
||||
}
|
||||
|
||||
private InputStream getGeoIpInputStream() throws IOException {
|
||||
InputStream in = appContext.getResources().getAssets().open("geoip");
|
||||
ZipInputStream zin = new ZipInputStream(in);
|
||||
if(zin.getNextEntry() == null) throw new IOException();
|
||||
return zin;
|
||||
}
|
||||
|
||||
private InputStream getConfigInputStream() throws IOException {
|
||||
return appContext.getResources().getAssets().open("torrc");
|
||||
}
|
||||
|
||||
private void copy(InputStream in, OutputStream out) throws IOException {
|
||||
byte[] buf = new byte[4096];
|
||||
while(true) {
|
||||
int read = in.read(buf);
|
||||
if(read == -1) break;
|
||||
out.write(buf, 0, read);
|
||||
}
|
||||
in.close();
|
||||
out.close();
|
||||
}
|
||||
|
||||
private boolean setExecutable(File f) {
|
||||
String[] command = { "chmod", "700", f.getAbsolutePath() };
|
||||
try {
|
||||
return Runtime.getRuntime().exec(command).waitFor() == 0;
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Interrupted while executing chmod");
|
||||
Thread.currentThread().interrupt();
|
||||
} catch(SecurityException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void tryToClose(InputStream in) {
|
||||
try {
|
||||
if(in != null) in.close();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void tryToClose(OutputStream out) {
|
||||
try {
|
||||
if(out != null) out.close();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void listFiles(File f) {
|
||||
if(f.isDirectory()) for(File f1 : f.listFiles()) listFiles(f1);
|
||||
else if(LOG.isLoggable(INFO)) LOG.info(f.getAbsolutePath());
|
||||
}
|
||||
|
||||
private byte[] read(File f) throws IOException {
|
||||
byte[] b = new byte[(int) f.length()];
|
||||
FileInputStream in = new FileInputStream(f);
|
||||
try {
|
||||
int offset = 0;
|
||||
while(offset < b.length) {
|
||||
int read = in.read(b, offset, b.length - offset);
|
||||
if(read == -1) throw new EOFException();
|
||||
offset += read;
|
||||
}
|
||||
return b;
|
||||
} finally {
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void bind() {
|
||||
// If there's already a port number stored in config, reuse it
|
||||
String portString = callback.getConfig().get("port");
|
||||
int port;
|
||||
if(StringUtils.isNullOrEmpty(portString)) port = 0;
|
||||
else port = Integer.parseInt(portString);
|
||||
// Bind a server socket to receive connections from the Tor process
|
||||
ServerSocket ss = null;
|
||||
try {
|
||||
ss = new ServerSocket();
|
||||
ss.bind(new InetSocketAddress("127.0.0.1", port));
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
tryToClose(ss);
|
||||
}
|
||||
if(!running) {
|
||||
tryToClose(ss);
|
||||
return;
|
||||
}
|
||||
socket = ss;
|
||||
// Store the port number
|
||||
final String localPort = String.valueOf(ss.getLocalPort());
|
||||
TransportConfig c = new TransportConfig();
|
||||
c.put("port", localPort);
|
||||
callback.mergeConfig(c);
|
||||
// Create a hidden service if necessary
|
||||
pluginExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
publishHiddenService(localPort);
|
||||
}
|
||||
});
|
||||
// Accept incoming hidden service connections from the Tor process
|
||||
acceptContactConnections(ss);
|
||||
}
|
||||
|
||||
private void tryToClose(ServerSocket ss) {
|
||||
try {
|
||||
if(ss != null) ss.close();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void publishHiddenService(final String port) {
|
||||
if(!running) return;
|
||||
if(!hostnameFile.exists()) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Creating hidden service");
|
||||
try {
|
||||
// Watch for the hostname file being created/updated
|
||||
hostnameFile.getParentFile().mkdirs();
|
||||
hostnameFile.createNewFile();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
FileObserver obs = new WriteObserver(hostnameFile, latch);
|
||||
obs.startWatching();
|
||||
// Use the control connection to update the Tor config
|
||||
List<String> config = Arrays.asList(
|
||||
"HiddenServiceDir " + torDirectory.getAbsolutePath(),
|
||||
"HiddenServicePort 80 127.0.0.1:" + port);
|
||||
controlConnection.setConf(config);
|
||||
controlConnection.saveConf();
|
||||
// Wait for the hostname file to be created/updated
|
||||
if(!latch.await(HOSTNAME_TIMEOUT, MILLISECONDS)) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Hidden service not created");
|
||||
listFiles(torDirectory);
|
||||
return;
|
||||
}
|
||||
if(!running) return;
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Interrupted while creating hidden service");
|
||||
}
|
||||
}
|
||||
// Publish the hidden service's onion hostname in transport properties
|
||||
try {
|
||||
String hostname = new String(read(hostnameFile), "UTF-8").trim();
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Hidden service " + hostname);
|
||||
TransportProperties p = new TransportProperties();
|
||||
p.put("onion", hostname);
|
||||
callback.mergeLocalProperties(p);
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptContactConnections(ServerSocket ss) {
|
||||
while(true) {
|
||||
Socket s;
|
||||
try {
|
||||
s = ss.accept();
|
||||
} catch(IOException e) {
|
||||
// This is expected when the socket is closed
|
||||
if(LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
|
||||
tryToClose(ss);
|
||||
return;
|
||||
}
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Connection received");
|
||||
TorTransportConnection conn = new TorTransportConnection(this, s);
|
||||
callback.incomingConnectionCreated(conn);
|
||||
if(!running) return;
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() throws IOException {
|
||||
running = false;
|
||||
if(socket != null) tryToClose(socket);
|
||||
try {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Stopping Tor");
|
||||
if(controlSocket == null)
|
||||
controlSocket = new Socket("127.0.0.1", CONTROL_PORT);
|
||||
if(controlConnection == null) {
|
||||
controlConnection = new TorControlConnection(controlSocket);
|
||||
controlConnection.authenticate(read(cookieFile));
|
||||
}
|
||||
controlConnection.shutdownTor("TERM");
|
||||
controlSocket.close();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Killing Tor");
|
||||
if(tor != null) tor.destroy();
|
||||
if(pid != -1) android.os.Process.killProcess(pid);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean shouldPoll() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public long getPollingInterval() {
|
||||
return pollingInterval;
|
||||
}
|
||||
|
||||
public void poll(Collection<ContactId> connected) {
|
||||
if(!running) return;
|
||||
Map<ContactId, TransportProperties> remote =
|
||||
callback.getRemoteProperties();
|
||||
for(final ContactId c : remote.keySet()) {
|
||||
if(connected.contains(c)) continue;
|
||||
pluginExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
connectAndCallBack(c);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void connectAndCallBack(ContactId c) {
|
||||
DuplexTransportConnection d = createConnection(c);
|
||||
if(d != null) callback.outgoingConnectionCreated(c, d);
|
||||
}
|
||||
|
||||
public DuplexTransportConnection createConnection(ContactId c) {
|
||||
if(!running) return null;
|
||||
TransportProperties p = callback.getRemoteProperties().get(c);
|
||||
if(p == null) return null;
|
||||
String onion = p.get("onion");
|
||||
if(StringUtils.isNullOrEmpty(onion)) return null;
|
||||
if(!ONION.matcher(onion).matches()) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Invalid hostname: " + onion);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Connecting to " + onion);
|
||||
Socks5Proxy proxy = new Socks5Proxy("127.0.0.1", SOCKS_PORT);
|
||||
proxy.resolveAddrLocally(false);
|
||||
Socket s = new SocksSocket(proxy, onion, 80);
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Connected to " + onion);
|
||||
return new TorTransportConnection(this, s);
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean supportsInvitations() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
|
||||
long timeout) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public void circuitStatus(String status, String circID, String path) {}
|
||||
|
||||
public void streamStatus(String status, String streamID, String target) {}
|
||||
|
||||
public void orConnStatus(String status, String orName) {}
|
||||
|
||||
public void bandwidthUsed(long read, long written) {}
|
||||
|
||||
public void newDescriptors(List<String> orList) {}
|
||||
|
||||
public void message(String severity, String msg) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info(severity + " " + msg);
|
||||
}
|
||||
|
||||
public void unrecognized(String type, String msg) {}
|
||||
|
||||
private static class WriteObserver extends FileObserver {
|
||||
|
||||
private final CountDownLatch latch;
|
||||
|
||||
private WriteObserver(File file, CountDownLatch latch) {
|
||||
super(file.getAbsolutePath(), CLOSE_WRITE);
|
||||
this.latch = latch;
|
||||
}
|
||||
|
||||
public void onEvent(int event, String path) {
|
||||
stopWatching();
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package net.sf.briar.plugins.tor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import net.sf.briar.api.TransportId;
|
||||
import net.sf.briar.api.lifecycle.ShutdownManager;
|
||||
import net.sf.briar.api.plugins.duplex.DuplexPlugin;
|
||||
import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
|
||||
import net.sf.briar.api.plugins.duplex.DuplexPluginFactory;
|
||||
import android.content.Context;
|
||||
|
||||
public class TorPluginFactory implements DuplexPluginFactory {
|
||||
|
||||
private static final int MAX_FRAME_LENGTH = 1024;
|
||||
private static final long MAX_LATENCY = 60 * 1000; // 1 minute
|
||||
private static final long POLLING_INTERVAL = 3 * 60 * 1000; // 3 minutes
|
||||
|
||||
private final Executor pluginExecutor;
|
||||
private final Context appContext;
|
||||
private final ShutdownManager shutdownManager;
|
||||
|
||||
public TorPluginFactory(Executor pluginExecutor, Context appContext,
|
||||
ShutdownManager shutdownManager) {
|
||||
this.pluginExecutor = pluginExecutor;
|
||||
this.appContext = appContext;
|
||||
this.shutdownManager = shutdownManager;
|
||||
}
|
||||
|
||||
public TransportId getId() {
|
||||
return TorPlugin.ID;
|
||||
}
|
||||
|
||||
public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
|
||||
return new TorPlugin(pluginExecutor,appContext, shutdownManager,
|
||||
callback, MAX_FRAME_LENGTH, MAX_LATENCY, POLLING_INTERVAL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package net.sf.briar.plugins.tor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
import net.sf.briar.api.plugins.Plugin;
|
||||
import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
|
||||
|
||||
class TorTransportConnection implements DuplexTransportConnection {
|
||||
|
||||
private final Plugin plugin;
|
||||
private final Socket socket;
|
||||
|
||||
TorTransportConnection(Plugin plugin, Socket socket) {
|
||||
this.plugin = plugin;
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
public int getMaxFrameLength() {
|
||||
return plugin.getMaxFrameLength();
|
||||
}
|
||||
|
||||
public long getMaxLatency() {
|
||||
return plugin.getMaxLatency();
|
||||
}
|
||||
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return socket.getInputStream();
|
||||
}
|
||||
|
||||
public OutputStream getOutputStream() throws IOException {
|
||||
return socket.getOutputStream();
|
||||
}
|
||||
|
||||
public boolean shouldFlush() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void dispose(boolean exception, boolean recognised)
|
||||
throws IOException {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user