diff --git a/.classpath b/.classpath index 89e4f57994904e367fc493c39a440fd12be8062c..644f1507aa69fc2aea6930b7a2b9e87082163048 100644 --- a/.classpath +++ b/.classpath @@ -22,5 +22,6 @@ <classpathentry kind="lib" path="lib/bluecove-gpl-2.1.0.jar" sourcepath="lib/source/bluecove-gpl-2.1.0-sources.jar"/> <classpathentry kind="lib" path="lib/bluecove-2.1.0-briar.jar" sourcepath="lib/source/bluecove-2.1.0-briar-sources.jar"/> <classpathentry kind="lib" path="lib/h2small-1.3.161.jar"/> + <classpathentry kind="lib" path="lib/silvertunnel.org_netlib.jar"/> <classpathentry kind="output" path="bin"/> </classpath> diff --git a/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java b/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java index 007106cb2c888ce4b778149aae1b83ff3bad8b4a..5b1f324f64f3028a621f849f9eae89e86382c9fb 100644 --- a/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java +++ b/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java @@ -39,7 +39,7 @@ class BluetoothPlugin implements DuplexPlugin { StringUtils.fromHexString("d99c9313c04417dcf22fc60d12a187ea" + "00a539fd260f08a13a0d8a900cde5e49"); - private static final TransportId id = new TransportId(TRANSPORT_ID); + private static final TransportId ID = new TransportId(TRANSPORT_ID); private static final Logger LOG = Logger.getLogger(BluetoothPlugin.class.getName()); @@ -65,7 +65,7 @@ class BluetoothPlugin implements DuplexPlugin { } public TransportId getId() { - return id; + return ID; } public void start() throws IOException { diff --git a/components/net/sf/briar/plugins/file/RemovableDrivePlugin.java b/components/net/sf/briar/plugins/file/RemovableDrivePlugin.java index a42eb152c10983c88d868bab08c77da8d4b0c42b..a5557332fb6aa1084928b646ef5f736a89f10b61 100644 --- a/components/net/sf/briar/plugins/file/RemovableDrivePlugin.java +++ b/components/net/sf/briar/plugins/file/RemovableDrivePlugin.java @@ -23,7 +23,7 @@ implements RemovableDriveMonitor.Callback { StringUtils.fromHexString("7c81bf5c9b1cd557685548c85f976bbd" + "e633d2418ea2e230e5710fb43c6f8cc0"); - private static final TransportId id = new TransportId(TRANSPORT_ID); + private static final TransportId ID = new TransportId(TRANSPORT_ID); private static final Logger LOG = Logger.getLogger(RemovableDrivePlugin.class.getName()); @@ -39,7 +39,7 @@ implements RemovableDriveMonitor.Callback { } public TransportId getId() { - return id; + return ID; } public void start() throws IOException { diff --git a/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java b/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java index 69d948dcb0899f3ae2d4fc890c7872b817329810..b4d36f13ab8cc838da5e1929ae58ee8d28aa607f 100644 --- a/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java +++ b/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java @@ -27,7 +27,7 @@ class SimpleSocketPlugin extends SocketPlugin { StringUtils.fromHexString("58c66d999e492b85065924acfd739d80" + "c65a62f87e5a4fc6c284f95908b9007d"); - private static final TransportId id = new TransportId(TRANSPORT_ID); + private static final TransportId ID = new TransportId(TRANSPORT_ID); private static final Logger LOG = Logger.getLogger(SimpleSocketPlugin.class.getName()); @@ -37,7 +37,7 @@ class SimpleSocketPlugin extends SocketPlugin { } public TransportId getId() { - return id; + return ID; } @Override diff --git a/components/net/sf/briar/plugins/tor/TorPlugin.java b/components/net/sf/briar/plugins/tor/TorPlugin.java new file mode 100644 index 0000000000000000000000000000000000000000..7000fc032f81b25dcf9dea7511746f578c1a306a --- /dev/null +++ b/components/net/sf/briar/plugins/tor/TorPlugin.java @@ -0,0 +1,229 @@ +package net.sf.briar.plugins.tor; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.logging.Level; +import java.util.logging.Logger; + +import net.sf.briar.api.ContactId; +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.plugins.PluginExecutor; +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.api.protocol.TransportId; +import net.sf.briar.util.StringUtils; + +import org.silvertunnel.netlib.api.NetFactory; +import org.silvertunnel.netlib.api.NetLayer; +import org.silvertunnel.netlib.api.NetLayerIDs; +import org.silvertunnel.netlib.api.NetServerSocket; +import org.silvertunnel.netlib.api.NetSocket; +import org.silvertunnel.netlib.api.util.TcpipNetAddress; +import org.silvertunnel.netlib.layer.tor.TorHiddenServicePortPrivateNetAddress; +import org.silvertunnel.netlib.layer.tor.TorHiddenServicePrivateNetAddress; +import org.silvertunnel.netlib.layer.tor.TorNetLayerUtil; +import org.silvertunnel.netlib.layer.tor.TorNetServerSocket; +import org.silvertunnel.netlib.layer.tor.util.Encryption; +import org.silvertunnel.netlib.layer.tor.util.RSAKeyPair; + +class TorPlugin implements DuplexPlugin { + + public static final byte[] TRANSPORT_ID = + StringUtils.fromHexString("f264721575cb7ee710772f35abeb3db4" + + "a91f474e14de346be296c2efc99effdd"); + + private static final TransportId ID = new TransportId(TRANSPORT_ID); + private static final Logger LOG = + Logger.getLogger(TorPlugin.class.getName()); + + private final Executor pluginExecutor; + private final DuplexPluginCallback callback; + private final long pollingInterval; + + private boolean running = false; // Locking: this + private TorNetServerSocket socket = null; // Locking: this + + TorPlugin(@PluginExecutor Executor pluginExecutor, + DuplexPluginCallback callback, long pollingInterval) { + this.pluginExecutor = pluginExecutor; + this.callback = callback; + this.pollingInterval = pollingInterval; + } + + public TransportId getId() { + return ID; + } + + public void start() throws IOException { + synchronized(this) { + running = true; + } + pluginExecutor.execute(new Runnable() { + public void run() { + bind(); + } + }); + } + + private void bind() { + // Retrieve the hidden service address, or create one if necessary + TorHiddenServicePrivateNetAddress addr; + TransportConfig c = callback.getConfig(); + String privateKey = c.get("privateKey"); + if(privateKey == null) { + addr = createHiddenServiceAddress(c); + } else { + TorNetLayerUtil util = TorNetLayerUtil.getInstance(); + try { + addr = util.parseTorHiddenServicePrivateNetAddressFromStrings( + privateKey, "", false); + } catch(IOException e) { + if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString()); + addr = createHiddenServiceAddress(c); + } + } + TorHiddenServicePortPrivateNetAddress addrPort = + new TorHiddenServicePortPrivateNetAddress(addr, 80); + // Connect to Tor + NetFactory netFactory = NetFactory.getInstance(); + NetLayer netLayer = netFactory.getNetLayerById(NetLayerIDs.TOR); + netLayer.waitUntilReady(); + // Publish the hidden service + TorNetServerSocket ss; + try { + ss = (TorNetServerSocket) netLayer.createNetServerSocket(null, + addrPort); + } catch(IOException e) { + if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString()); + return; + } + synchronized(this) { + if(!running) { + tryToClose(ss); + return; + } + socket = ss; + } + String onion = addr.getPublicOnionHostname(); + if(LOG.isLoggable(Level.INFO)) LOG.info("Listening on " + onion); + TransportProperties p = callback.getLocalProperties(); + p.put("onion", onion); + callback.setLocalProperties(p); + acceptContactConnections(ss); + } + + private TorHiddenServicePrivateNetAddress createHiddenServiceAddress( + TransportConfig c) { + TorNetLayerUtil util = TorNetLayerUtil.getInstance(); + TorHiddenServicePrivateNetAddress addr = + util.createNewTorHiddenServicePrivateNetAddress(); + RSAKeyPair keyPair = addr.getKeyPair(); + String privateKey = Encryption.getPEMStringFromRSAKeyPair(keyPair); + c.put("privateKey", privateKey); + callback.setConfig(c); + return addr; + } + + private void tryToClose(NetServerSocket ss) { + try { + ss.close(); + } catch(IOException e) { + if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString()); + } + } + + private void acceptContactConnections(NetServerSocket ss) { + while(true) { + NetSocket s; + try { + s = ss.accept(); + } catch(IOException e) { + // This is expected when the socket is closed + if(LOG.isLoggable(Level.INFO)) LOG.info(e.toString()); + tryToClose(ss); + return; + } + TorTransportConnection conn = new TorTransportConnection(s); + callback.incomingConnectionCreated(conn); + synchronized(this) { + if(!running) return; + } + } + } + + public synchronized void stop() throws IOException { + running = false; + if(socket != null) { + tryToClose(socket); + socket = null; + } + } + + public boolean shouldPoll() { + return true; + } + + public long getPollingInterval() { + return pollingInterval; + } + + public void poll(Collection<ContactId> connected) { + synchronized(this) { + 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 boolean supportsInvitations() { + return false; + } + + public DuplexTransportConnection createConnection(ContactId c) { + synchronized(this) { + if(!running) return null; + } + TransportProperties p = callback.getRemoteProperties().get(c); + if(p == null) return null; + String onion = p.get("onion"); + String portString = p.get("port"); + if(onion == null || portString == null) return null; + try { + int port = Integer.parseInt(portString); + TcpipNetAddress addr = new TcpipNetAddress(onion, port); + NetFactory netFactory = NetFactory.getInstance(); + NetLayer netLayer = netFactory.getNetLayerById(NetLayerIDs.TOR); + netLayer.waitUntilReady(); + NetSocket s = netLayer.createNetSocket(null, null, addr); + return new TorTransportConnection(s); + } catch(IOException e) { + if(LOG.isLoggable(Level.INFO)) LOG.info(e.toString()); + return null; + } + } + + public DuplexTransportConnection sendInvitation(int code, long timeout) { + throw new UnsupportedOperationException(); + } + + public DuplexTransportConnection acceptInvitation(int code, long timeout) { + throw new UnsupportedOperationException(); + } +} diff --git a/components/net/sf/briar/plugins/tor/TorTransportConnection.java b/components/net/sf/briar/plugins/tor/TorTransportConnection.java new file mode 100644 index 0000000000000000000000000000000000000000..4a8d74256fae07c349346a2dd5326909d756ec8e --- /dev/null +++ b/components/net/sf/briar/plugins/tor/TorTransportConnection.java @@ -0,0 +1,43 @@ +package net.sf.briar.plugins.tor; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +import net.sf.briar.api.plugins.duplex.DuplexTransportConnection; + +import org.silvertunnel.netlib.api.NetSocket; + +class TorTransportConnection implements DuplexTransportConnection { + + private static final Logger LOG = + Logger.getLogger(TorTransportConnection.class.getName()); + + private final NetSocket socket; + + TorTransportConnection(NetSocket socket) { + this.socket = socket; + } + + 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) { + try { + socket.close(); + } catch(IOException e) { + if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString()); + } + } +} diff --git a/lib/silvertunnel.org_netlib.jar b/lib/silvertunnel.org_netlib.jar new file mode 100644 index 0000000000000000000000000000000000000000..9a779d318c1b948237d132dde93304d328d63ef4 Binary files /dev/null and b/lib/silvertunnel.org_netlib.jar differ diff --git a/test/net/sf/briar/plugins/tor/TorPluginTest.java b/test/net/sf/briar/plugins/tor/TorPluginTest.java new file mode 100644 index 0000000000000000000000000000000000000000..aef1f7f6bd8c61b6d60839b60eb792307e7b37b1 --- /dev/null +++ b/test/net/sf/briar/plugins/tor/TorPluginTest.java @@ -0,0 +1,79 @@ +package net.sf.briar.plugins.tor; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.plugins.duplex.DuplexPluginCallback; +import net.sf.briar.api.plugins.duplex.DuplexTransportConnection; + +import org.junit.Test; + +public class TorPluginTest extends BriarTestCase { + + @Test + public void testCreateHiddenService() throws Exception { + Callback callback = new Callback(); + Executor e = Executors.newCachedThreadPool(); + TorPlugin plugin = new TorPlugin(e, callback, 0L); + plugin.start(); + // The plugin should have created a hidden service + callback.latch.await(5, TimeUnit.MINUTES); + String onion = callback.local.get("onion"); + assertNotNull(onion); + assertTrue(onion.endsWith(".onion")); + } + + private static class Callback implements DuplexPluginCallback { + + private final Map<ContactId, TransportProperties> remote = + new HashMap<ContactId, TransportProperties>(); + private final CountDownLatch latch = new CountDownLatch(1); + + private TransportConfig config = new TransportConfig(); + private TransportProperties local = new TransportProperties(); + + public TransportConfig getConfig() { + return config; + } + + public TransportProperties getLocalProperties() { + return local; + } + + public Map<ContactId, TransportProperties> getRemoteProperties() { + return remote; + } + + public void setConfig(TransportConfig c) { + config = c; + } + + public void setLocalProperties(TransportProperties p) { + latch.countDown(); + 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 c, + DuplexTransportConnection d) {} + } +}