mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-16 20:59:54 +01:00
Changed the root package from net.sf.briar to org.briarproject.
This commit is contained in:
362
briar-core/src/org/briarproject/plugins/PluginManagerImpl.java
Normal file
362
briar-core/src/org/briarproject/plugins/PluginManagerImpl.java
Normal file
@@ -0,0 +1,362 @@
|
||||
package org.briarproject.plugins;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.briarproject.api.ContactId;
|
||||
import org.briarproject.api.TransportConfig;
|
||||
import org.briarproject.api.TransportId;
|
||||
import org.briarproject.api.TransportProperties;
|
||||
import org.briarproject.api.db.DatabaseComponent;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.plugins.Plugin;
|
||||
import org.briarproject.api.plugins.PluginCallback;
|
||||
import org.briarproject.api.plugins.PluginExecutor;
|
||||
import org.briarproject.api.plugins.PluginManager;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPlugin;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginConfig;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
|
||||
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
|
||||
import org.briarproject.api.plugins.simplex.SimplexPlugin;
|
||||
import org.briarproject.api.plugins.simplex.SimplexPluginCallback;
|
||||
import org.briarproject.api.plugins.simplex.SimplexPluginConfig;
|
||||
import org.briarproject.api.plugins.simplex.SimplexPluginFactory;
|
||||
import org.briarproject.api.plugins.simplex.SimplexTransportReader;
|
||||
import org.briarproject.api.plugins.simplex.SimplexTransportWriter;
|
||||
import org.briarproject.api.transport.ConnectionDispatcher;
|
||||
import org.briarproject.api.ui.UiCallback;
|
||||
|
||||
// FIXME: Don't make alien calls with a lock held (that includes waiting on a
|
||||
// latch that depends on an alien call)
|
||||
class PluginManagerImpl implements PluginManager {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(PluginManagerImpl.class.getName());
|
||||
|
||||
private final Executor pluginExecutor;
|
||||
private final SimplexPluginConfig simplexPluginConfig;
|
||||
private final DuplexPluginConfig duplexPluginConfig;
|
||||
private final DatabaseComponent db;
|
||||
private final Poller poller;
|
||||
private final ConnectionDispatcher dispatcher;
|
||||
private final UiCallback uiCallback;
|
||||
private final List<SimplexPlugin> simplexPlugins;
|
||||
private final List<DuplexPlugin> duplexPlugins;
|
||||
|
||||
@Inject
|
||||
PluginManagerImpl(@PluginExecutor Executor pluginExecutor,
|
||||
SimplexPluginConfig simplexPluginConfig,
|
||||
DuplexPluginConfig duplexPluginConfig, DatabaseComponent db,
|
||||
Poller poller, ConnectionDispatcher dispatcher,
|
||||
UiCallback uiCallback) {
|
||||
this.pluginExecutor = pluginExecutor;
|
||||
this.simplexPluginConfig = simplexPluginConfig;
|
||||
this.duplexPluginConfig = duplexPluginConfig;
|
||||
this.db = db;
|
||||
this.poller = poller;
|
||||
this.dispatcher = dispatcher;
|
||||
this.uiCallback = uiCallback;
|
||||
simplexPlugins = new CopyOnWriteArrayList<SimplexPlugin>();
|
||||
duplexPlugins = new CopyOnWriteArrayList<DuplexPlugin>();
|
||||
}
|
||||
|
||||
public synchronized boolean start() {
|
||||
// Instantiate and start the simplex plugins
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Starting simplex plugins");
|
||||
Collection<SimplexPluginFactory> sFactories =
|
||||
simplexPluginConfig.getFactories();
|
||||
final CountDownLatch sLatch = new CountDownLatch(sFactories.size());
|
||||
for(SimplexPluginFactory factory : sFactories)
|
||||
pluginExecutor.execute(new SimplexPluginStarter(factory, sLatch));
|
||||
// Instantiate and start the duplex plugins
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Starting duplex plugins");
|
||||
Collection<DuplexPluginFactory> dFactories =
|
||||
duplexPluginConfig.getFactories();
|
||||
final CountDownLatch dLatch = new CountDownLatch(dFactories.size());
|
||||
for(DuplexPluginFactory factory : dFactories)
|
||||
pluginExecutor.execute(new DuplexPluginStarter(factory, dLatch));
|
||||
// Wait for the plugins to start
|
||||
try {
|
||||
sLatch.await();
|
||||
dLatch.await();
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Interrupted while starting plugins");
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
// Start the poller
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Starting poller");
|
||||
List<Plugin> plugins = new ArrayList<Plugin>();
|
||||
plugins.addAll(simplexPlugins);
|
||||
plugins.addAll(duplexPlugins);
|
||||
poller.start(Collections.unmodifiableList(plugins));
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized boolean stop() {
|
||||
// Stop the poller
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Stopping poller");
|
||||
poller.stop();
|
||||
int plugins = simplexPlugins.size() + duplexPlugins.size();
|
||||
final CountDownLatch latch = new CountDownLatch(plugins);
|
||||
// Stop the simplex plugins
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Stopping simplex plugins");
|
||||
for(SimplexPlugin plugin : simplexPlugins)
|
||||
pluginExecutor.execute(new PluginStopper(plugin, latch));
|
||||
// Stop the duplex plugins
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Stopping duplex plugins");
|
||||
for(DuplexPlugin plugin : duplexPlugins)
|
||||
pluginExecutor.execute(new PluginStopper(plugin, latch));
|
||||
simplexPlugins.clear();
|
||||
duplexPlugins.clear();
|
||||
// Wait for all the plugins to stop
|
||||
try {
|
||||
latch.await();
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.warning("Interrupted while stopping plugins");
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public Collection<DuplexPlugin> getInvitationPlugins() {
|
||||
List<DuplexPlugin> supported = new ArrayList<DuplexPlugin>();
|
||||
for(DuplexPlugin d : duplexPlugins)
|
||||
if(d.supportsInvitations()) supported.add(d);
|
||||
return Collections.unmodifiableList(supported);
|
||||
}
|
||||
|
||||
private class SimplexPluginStarter implements Runnable {
|
||||
|
||||
private final SimplexPluginFactory factory;
|
||||
private final CountDownLatch latch;
|
||||
|
||||
private SimplexPluginStarter(SimplexPluginFactory factory,
|
||||
CountDownLatch latch) {
|
||||
this.factory = factory;
|
||||
this.latch = latch;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
TransportId id = factory.getId();
|
||||
SimplexCallback callback = new SimplexCallback(id);
|
||||
SimplexPlugin plugin = factory.createPlugin(callback);
|
||||
if(plugin == null) {
|
||||
if(LOG.isLoggable(INFO)) {
|
||||
String name = factory.getClass().getSimpleName();
|
||||
LOG.info(name + " did not create a plugin");
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
db.addTransport(id, plugin.getMaxLatency());
|
||||
} catch(DbException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if(plugin.start()) {
|
||||
simplexPlugins.add(plugin);
|
||||
} else {
|
||||
if(LOG.isLoggable(INFO)) {
|
||||
String name = plugin.getClass().getSimpleName();
|
||||
LOG.info(name + " did not start");
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DuplexPluginStarter implements Runnable {
|
||||
|
||||
private final DuplexPluginFactory factory;
|
||||
private final CountDownLatch latch;
|
||||
|
||||
private DuplexPluginStarter(DuplexPluginFactory factory,
|
||||
CountDownLatch latch) {
|
||||
this.factory = factory;
|
||||
this.latch = latch;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
TransportId id = factory.getId();
|
||||
DuplexCallback callback = new DuplexCallback(id);
|
||||
DuplexPlugin plugin = factory.createPlugin(callback);
|
||||
if(plugin == null) {
|
||||
if(LOG.isLoggable(INFO)) {
|
||||
String name = factory.getClass().getSimpleName();
|
||||
LOG.info(name + " did not create a plugin");
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
db.addTransport(id, plugin.getMaxLatency());
|
||||
} catch(DbException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if(plugin.start()) {
|
||||
duplexPlugins.add(plugin);
|
||||
} else {
|
||||
if(LOG.isLoggable(INFO)) {
|
||||
String name = plugin.getClass().getSimpleName();
|
||||
LOG.info(name + " did not start");
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PluginStopper implements Runnable {
|
||||
|
||||
private final Plugin plugin;
|
||||
private final CountDownLatch latch;
|
||||
|
||||
private PluginStopper(Plugin plugin, CountDownLatch latch) {
|
||||
this.plugin = plugin;
|
||||
this.latch = latch;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
plugin.stop();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class PluginCallbackImpl implements PluginCallback {
|
||||
|
||||
protected final TransportId id;
|
||||
|
||||
protected PluginCallbackImpl(TransportId id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public TransportConfig getConfig() {
|
||||
try {
|
||||
return db.getConfig(id);
|
||||
} catch(DbException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return new TransportConfig();
|
||||
}
|
||||
}
|
||||
|
||||
public TransportProperties getLocalProperties() {
|
||||
try {
|
||||
TransportProperties p = db.getLocalProperties(id);
|
||||
return p == null ? new TransportProperties() : p;
|
||||
} catch(DbException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return new TransportProperties();
|
||||
}
|
||||
}
|
||||
|
||||
public Map<ContactId, TransportProperties> getRemoteProperties() {
|
||||
try {
|
||||
return db.getRemoteProperties(id);
|
||||
} catch(DbException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
||||
public void mergeConfig(TransportConfig c) {
|
||||
try {
|
||||
db.mergeConfig(id, c);
|
||||
} catch(DbException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void mergeLocalProperties(TransportProperties p) {
|
||||
try {
|
||||
db.mergeLocalProperties(id, p);
|
||||
} catch(DbException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public int showChoice(String[] options, String... message) {
|
||||
return uiCallback.showChoice(options, message);
|
||||
}
|
||||
|
||||
public boolean showConfirmationMessage(String... message) {
|
||||
return uiCallback.showConfirmationMessage(message);
|
||||
}
|
||||
|
||||
public void showMessage(String... message) {
|
||||
uiCallback.showMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
private class SimplexCallback extends PluginCallbackImpl
|
||||
implements SimplexPluginCallback {
|
||||
|
||||
private SimplexCallback(TransportId id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
public void readerCreated(SimplexTransportReader r) {
|
||||
dispatcher.dispatchReader(id, r);
|
||||
}
|
||||
|
||||
public void writerCreated(ContactId c, SimplexTransportWriter w) {
|
||||
dispatcher.dispatchWriter(c, id, w);
|
||||
}
|
||||
}
|
||||
|
||||
private class DuplexCallback extends PluginCallbackImpl
|
||||
implements DuplexPluginCallback {
|
||||
|
||||
private DuplexCallback(TransportId id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
public void incomingConnectionCreated(DuplexTransportConnection d) {
|
||||
dispatcher.dispatchIncomingConnection(id, d);
|
||||
}
|
||||
|
||||
public void outgoingConnectionCreated(ContactId c,
|
||||
DuplexTransportConnection d) {
|
||||
dispatcher.dispatchOutgoingConnection(c, id, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
briar-core/src/org/briarproject/plugins/PluginsModule.java
Normal file
52
briar-core/src/org/briarproject/plugins/PluginsModule.java
Normal file
@@ -0,0 +1,52 @@
|
||||
package org.briarproject.plugins;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.RejectedExecutionHandler;
|
||||
import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.api.plugins.PluginExecutor;
|
||||
import org.briarproject.api.plugins.PluginManager;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Provides;
|
||||
|
||||
public class PluginsModule extends AbstractModule {
|
||||
|
||||
private final ExecutorService pluginExecutor;
|
||||
|
||||
public PluginsModule() {
|
||||
// The thread pool is unbounded, so use direct handoff
|
||||
BlockingQueue<Runnable> queue = new SynchronousQueue<Runnable>();
|
||||
// Discard tasks that are submitted during shutdown
|
||||
RejectedExecutionHandler policy =
|
||||
new ThreadPoolExecutor.DiscardPolicy();
|
||||
// Create threads as required and keep them in the pool for 60 seconds
|
||||
pluginExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
|
||||
60, SECONDS, queue, policy);
|
||||
}
|
||||
|
||||
protected void configure() {
|
||||
bind(Poller.class).to(PollerImpl.class);
|
||||
}
|
||||
|
||||
@Provides @Singleton
|
||||
PluginManager getPluginManager(LifecycleManager lifecycleManager,
|
||||
PluginManagerImpl pluginManager) {
|
||||
lifecycleManager.register(pluginManager);
|
||||
return pluginManager;
|
||||
}
|
||||
|
||||
@Provides @Singleton @PluginExecutor
|
||||
Executor getPluginExecutor(LifecycleManager lifecycleManager) {
|
||||
lifecycleManager.registerForShutdown(pluginExecutor);
|
||||
return pluginExecutor;
|
||||
}
|
||||
}
|
||||
14
briar-core/src/org/briarproject/plugins/Poller.java
Normal file
14
briar-core/src/org/briarproject/plugins/Poller.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package org.briarproject.plugins;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.briarproject.api.plugins.Plugin;
|
||||
|
||||
interface Poller {
|
||||
|
||||
/** Starts a new thread to poll the given collection of plugins. */
|
||||
void start(Collection<Plugin> plugins);
|
||||
|
||||
/** Tells the poller thread to exit. */
|
||||
void stop();
|
||||
}
|
||||
126
briar-core/src/org/briarproject/plugins/PollerImpl.java
Normal file
126
briar-core/src/org/briarproject/plugins/PollerImpl.java
Normal file
@@ -0,0 +1,126 @@
|
||||
package org.briarproject.plugins;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.briarproject.api.ContactId;
|
||||
import org.briarproject.api.plugins.Plugin;
|
||||
import org.briarproject.api.plugins.PluginExecutor;
|
||||
import org.briarproject.api.system.Clock;
|
||||
import org.briarproject.api.transport.ConnectionRegistry;
|
||||
|
||||
class PollerImpl implements Poller, Runnable {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(PollerImpl.class.getName());
|
||||
|
||||
private final Executor pluginExecutor;
|
||||
private final ConnectionRegistry connRegistry;
|
||||
private final Clock clock;
|
||||
private final SortedSet<PollTime> pollTimes;
|
||||
|
||||
@Inject
|
||||
PollerImpl(@PluginExecutor Executor pluginExecutor,
|
||||
ConnectionRegistry connRegistry, Clock clock) {
|
||||
this.pluginExecutor = pluginExecutor;
|
||||
this.connRegistry = connRegistry;
|
||||
this.clock = clock;
|
||||
pollTimes = new TreeSet<PollTime>();
|
||||
}
|
||||
|
||||
public synchronized void start(Collection<Plugin> plugins) {
|
||||
for(Plugin plugin : plugins) schedule(plugin, true);
|
||||
new Thread(this, "Poller").start();
|
||||
}
|
||||
|
||||
private synchronized void schedule(Plugin plugin, boolean randomise) {
|
||||
if(plugin.shouldPoll()) {
|
||||
long now = clock.currentTimeMillis();
|
||||
long interval = plugin.getPollingInterval();
|
||||
// Randomise intervals at startup to spread out connection attempts
|
||||
if(randomise) interval = (long) (interval * Math.random());
|
||||
pollTimes.add(new PollTime(now + interval, plugin));
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
pollTimes.clear();
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
public void run() {
|
||||
while(true) {
|
||||
synchronized(this) {
|
||||
if(pollTimes.isEmpty()) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Finished polling");
|
||||
return;
|
||||
}
|
||||
long now = clock.currentTimeMillis();
|
||||
final PollTime p = pollTimes.first();
|
||||
if(now >= p.time) {
|
||||
boolean removed = pollTimes.remove(p);
|
||||
assert removed;
|
||||
final Collection<ContactId> connected =
|
||||
connRegistry.getConnectedContacts(p.plugin.getId());
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Polling " + p.plugin.getClass().getName());
|
||||
pluginExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
p.plugin.poll(connected);
|
||||
}
|
||||
});
|
||||
schedule(p.plugin, false);
|
||||
} else {
|
||||
try {
|
||||
wait(p.time - now);
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Interrupted while waiting to poll");
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class PollTime implements Comparable<PollTime> {
|
||||
|
||||
private final long time;
|
||||
private final Plugin plugin;
|
||||
|
||||
private PollTime(long time, Plugin plugin) {
|
||||
this.time = time;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
// Must be consistent with equals()
|
||||
public int compareTo(PollTime p) {
|
||||
if(time < p.time) return -1;
|
||||
if(time > p.time) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Must be consistent with equals()
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (int) (time ^ (time >>> 32)) ^ plugin.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(o instanceof PollTime) {
|
||||
PollTime p = (PollTime) o;
|
||||
return time == p.time && plugin == p.plugin;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
122
briar-core/src/org/briarproject/plugins/file/FilePlugin.java
Normal file
122
briar-core/src/org/briarproject/plugins/file/FilePlugin.java
Normal file
@@ -0,0 +1,122 @@
|
||||
package org.briarproject.plugins.file;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.api.transport.TransportConstants.MIN_CONNECTION_LENGTH;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.briarproject.api.ContactId;
|
||||
import org.briarproject.api.plugins.simplex.SimplexPlugin;
|
||||
import org.briarproject.api.plugins.simplex.SimplexPluginCallback;
|
||||
import org.briarproject.api.plugins.simplex.SimplexTransportReader;
|
||||
import org.briarproject.api.plugins.simplex.SimplexTransportWriter;
|
||||
import org.briarproject.api.system.FileUtils;
|
||||
|
||||
public abstract class FilePlugin implements SimplexPlugin {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(FilePlugin.class.getName());
|
||||
|
||||
protected final Executor pluginExecutor;
|
||||
protected final FileUtils fileUtils;
|
||||
protected final SimplexPluginCallback callback;
|
||||
protected final int maxFrameLength;
|
||||
protected final long maxLatency;
|
||||
|
||||
protected volatile boolean running = false;
|
||||
|
||||
protected abstract File chooseOutputDirectory();
|
||||
protected abstract Collection<File> findFilesByName(String filename);
|
||||
protected abstract void writerFinished(File f);
|
||||
protected abstract void readerFinished(File f);
|
||||
|
||||
protected FilePlugin(Executor pluginExecutor, FileUtils fileUtils,
|
||||
SimplexPluginCallback callback, int maxFrameLength,
|
||||
long maxLatency) {
|
||||
this.pluginExecutor = pluginExecutor;
|
||||
this.fileUtils = fileUtils;
|
||||
this.callback = callback;
|
||||
this.maxFrameLength = maxFrameLength;
|
||||
this.maxLatency = maxLatency;
|
||||
}
|
||||
|
||||
public int getMaxFrameLength() {
|
||||
return maxFrameLength;
|
||||
}
|
||||
|
||||
public long getMaxLatency() {
|
||||
return maxLatency;
|
||||
}
|
||||
|
||||
public SimplexTransportReader createReader(ContactId c) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public SimplexTransportWriter createWriter(ContactId c) {
|
||||
if(!running) return null;
|
||||
return createWriter(createConnectionFilename());
|
||||
}
|
||||
|
||||
private String createConnectionFilename() {
|
||||
StringBuilder s = new StringBuilder(12);
|
||||
for(int i = 0; i < 8; i++) s.append((char) ('a' + Math.random() * 26));
|
||||
s.append(".dat");
|
||||
return s.toString();
|
||||
}
|
||||
|
||||
// Package access for testing
|
||||
boolean isPossibleConnectionFilename(String filename) {
|
||||
return filename.toLowerCase().matches("[a-z]{8}\\.dat");
|
||||
}
|
||||
|
||||
private SimplexTransportWriter createWriter(String filename) {
|
||||
if(!running) return null;
|
||||
File dir = chooseOutputDirectory();
|
||||
if(dir == null || !dir.exists() || !dir.isDirectory()) return null;
|
||||
File f = new File(dir, filename);
|
||||
try {
|
||||
long capacity = fileUtils.getFreeSpace(dir);
|
||||
if(capacity < MIN_CONNECTION_LENGTH) return null;
|
||||
OutputStream out = new FileOutputStream(f);
|
||||
return new FileTransportWriter(f, out, capacity, this);
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
f.delete();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected void createReaderFromFile(final File f) {
|
||||
if(!running) return;
|
||||
pluginExecutor.execute(new ReaderCreator(f));
|
||||
}
|
||||
|
||||
private class ReaderCreator implements Runnable {
|
||||
|
||||
private final File file;
|
||||
|
||||
private ReaderCreator(File file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
if(isPossibleConnectionFilename(file.getName())) {
|
||||
try {
|
||||
FileInputStream in = new FileInputStream(file);
|
||||
callback.readerCreated(new FileTransportReader(file, in,
|
||||
FilePlugin.this));
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.briarproject.plugins.file;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.briarproject.api.plugins.simplex.SimplexTransportReader;
|
||||
|
||||
class FileTransportReader implements SimplexTransportReader {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(FileTransportReader.class.getName());
|
||||
|
||||
private final File file;
|
||||
private final InputStream in;
|
||||
private final FilePlugin plugin;
|
||||
|
||||
FileTransportReader(File file, InputStream in, FilePlugin plugin) {
|
||||
this.file = file;
|
||||
this.in = in;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public int getMaxFrameLength() {
|
||||
return plugin.getMaxFrameLength();
|
||||
}
|
||||
|
||||
public InputStream getInputStream() {
|
||||
return in;
|
||||
}
|
||||
|
||||
public void dispose(boolean exception, boolean recognised) {
|
||||
try {
|
||||
in.close();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
if(recognised) {
|
||||
file.delete();
|
||||
plugin.readerFinished(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.briarproject.plugins.file;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.briarproject.api.plugins.simplex.SimplexTransportWriter;
|
||||
|
||||
class FileTransportWriter implements SimplexTransportWriter {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(FileTransportWriter.class.getName());
|
||||
|
||||
private final File file;
|
||||
private final OutputStream out;
|
||||
private final long capacity;
|
||||
private final FilePlugin plugin;
|
||||
|
||||
FileTransportWriter(File file, OutputStream out, long capacity,
|
||||
FilePlugin plugin) {
|
||||
this.file = file;
|
||||
this.out = out;
|
||||
this.capacity = capacity;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public long getCapacity() {
|
||||
return capacity;
|
||||
}
|
||||
|
||||
public int getMaxFrameLength() {
|
||||
return plugin.getMaxFrameLength();
|
||||
}
|
||||
|
||||
public long getMaxLatency() {
|
||||
return plugin.getMaxLatency();
|
||||
}
|
||||
|
||||
public OutputStream getOutputStream() {
|
||||
return out;
|
||||
}
|
||||
|
||||
public boolean shouldFlush() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void dispose(boolean exception) {
|
||||
try {
|
||||
out.close();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
if(exception) file.delete();
|
||||
else plugin.writerFinished(file);
|
||||
}
|
||||
}
|
||||
345
briar-core/src/org/briarproject/plugins/tcp/LanTcpPlugin.java
Normal file
345
briar-core/src/org/briarproject/plugins/tcp/LanTcpPlugin.java
Normal file
@@ -0,0 +1,345 @@
|
||||
package org.briarproject.plugins.tcp;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.MulticastSocket;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.briarproject.api.TransportId;
|
||||
import org.briarproject.api.TransportProperties;
|
||||
import org.briarproject.api.crypto.PseudoRandom;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
|
||||
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
|
||||
import org.briarproject.api.system.Clock;
|
||||
import org.briarproject.util.ByteUtils;
|
||||
import org.briarproject.util.LatchedReference;
|
||||
import org.briarproject.util.StringUtils;
|
||||
|
||||
/** A socket plugin that supports exchanging invitations over a LAN. */
|
||||
class LanTcpPlugin extends TcpPlugin {
|
||||
|
||||
static final byte[] TRANSPORT_ID =
|
||||
StringUtils.fromHexString("0d79357fd7f74d66c2f6f6ad0f7fff81"
|
||||
+ "d21c53a43b90b0507ed0683872d8e2fc"
|
||||
+ "5a88e8f953638228dc26669639757bbf");
|
||||
static final TransportId ID = new TransportId(TRANSPORT_ID);
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(LanTcpPlugin.class.getName());
|
||||
private static final int MULTICAST_INTERVAL = 1000; // 1 second
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
LanTcpPlugin(Executor pluginExecutor, Clock clock,
|
||||
DuplexPluginCallback callback, int maxFrameLength, long maxLatency,
|
||||
long pollingInterval) {
|
||||
super(pluginExecutor, callback, maxFrameLength, maxLatency,
|
||||
pollingInterval);
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public TransportId getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return "LAN_TCP_PLUGIN_NAME";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<SocketAddress> getLocalSocketAddresses() {
|
||||
List<SocketAddress> addrs = new ArrayList<SocketAddress>();
|
||||
// Prefer a previously used address and port if available
|
||||
TransportProperties p = callback.getLocalProperties();
|
||||
String addrString = p.get("address");
|
||||
String portString = p.get("port");
|
||||
InetAddress addr = null;
|
||||
if(!StringUtils.isNullOrEmpty(addrString) &&
|
||||
!StringUtils.isNullOrEmpty(portString)) {
|
||||
try {
|
||||
addr = InetAddress.getByName(addrString);
|
||||
int port = Integer.parseInt(portString);
|
||||
addrs.add(new InetSocketAddress(addr, port));
|
||||
addrs.add(new InetSocketAddress(addr, 0));
|
||||
} catch(NumberFormatException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
} catch(UnknownHostException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
List<NetworkInterface> ifaces;
|
||||
try {
|
||||
ifaces = Collections.list(NetworkInterface.getNetworkInterfaces());
|
||||
} catch(SocketException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return addrs;
|
||||
}
|
||||
// Prefer interfaces with link-local or site-local addresses
|
||||
for(NetworkInterface iface : ifaces) {
|
||||
for(InetAddress a : Collections.list(iface.getInetAddresses())) {
|
||||
if(addr != null && a.equals(addr)) continue;
|
||||
if(a instanceof Inet6Address) continue;
|
||||
if(a.isLoopbackAddress()) continue;
|
||||
boolean link = a.isLinkLocalAddress();
|
||||
boolean site = a.isSiteLocalAddress();
|
||||
if(link || site) addrs.add(new InetSocketAddress(a, 0));
|
||||
}
|
||||
}
|
||||
// Accept interfaces without link-local or site-local addresses
|
||||
for(NetworkInterface iface : ifaces) {
|
||||
for(InetAddress a : Collections.list(iface.getInetAddresses())) {
|
||||
if(addr != null && a.equals(addr)) continue;
|
||||
if(a instanceof Inet6Address) continue;
|
||||
if(a.isLoopbackAddress()) continue;
|
||||
boolean link = a.isLinkLocalAddress();
|
||||
boolean site = a.isSiteLocalAddress();
|
||||
if(!link && !site) addrs.add(new InetSocketAddress(a, 0));
|
||||
}
|
||||
}
|
||||
return addrs;
|
||||
}
|
||||
|
||||
public boolean supportsInvitations() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
|
||||
long timeout) {
|
||||
if(!running) return null;
|
||||
// Use the invitation codes to generate the group address and port
|
||||
InetSocketAddress group = chooseMulticastGroup(r);
|
||||
// Bind a multicast socket for sending and receiving packets
|
||||
InetAddress iface = null;
|
||||
MulticastSocket ms = null;
|
||||
try {
|
||||
iface = chooseInvitationInterface();
|
||||
if(iface == null) return null;
|
||||
ms = new MulticastSocket(group.getPort());
|
||||
ms.setInterface(iface);
|
||||
ms.joinGroup(group.getAddress());
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
if(ms != null) tryToClose(ms, group.getAddress());
|
||||
return null;
|
||||
}
|
||||
// Bind a server socket for receiving invitation connections
|
||||
ServerSocket ss = null;
|
||||
try {
|
||||
ss = new ServerSocket();
|
||||
ss.bind(new InetSocketAddress(iface, 0));
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
if(ss != null) tryToClose(ss);
|
||||
return null;
|
||||
}
|
||||
// Start the listener threads
|
||||
LatchedReference<Socket> socketLatch = new LatchedReference<Socket>();
|
||||
new MulticastListenerThread(socketLatch, ms, iface).start();
|
||||
new TcpListenerThread(socketLatch, ss).start();
|
||||
// Send packets until a connection is made or we run out of time
|
||||
byte[] buffer = new byte[2];
|
||||
ByteUtils.writeUint16(ss.getLocalPort(), buffer, 0);
|
||||
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
|
||||
packet.setAddress(group.getAddress());
|
||||
packet.setPort(group.getPort());
|
||||
long now = clock.currentTimeMillis();
|
||||
long end = now + timeout;
|
||||
try {
|
||||
while(now < end && running) {
|
||||
// Send a packet
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Sending multicast packet");
|
||||
ms.send(packet);
|
||||
// Wait for an incoming or outgoing connection
|
||||
try {
|
||||
Socket s = socketLatch.waitForReference(MULTICAST_INTERVAL);
|
||||
if(s != null) return new TcpTransportConnection(this, s);
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Interrupted while exchanging invitations");
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
}
|
||||
now = clock.currentTimeMillis();
|
||||
}
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
} finally {
|
||||
// Closing the sockets will terminate the listener threads
|
||||
tryToClose(ms, group.getAddress());
|
||||
tryToClose(ss);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private InetSocketAddress chooseMulticastGroup(PseudoRandom r) {
|
||||
byte[] b = r.nextBytes(5);
|
||||
// The group address is 239.random.random.random, excluding 0 and 255
|
||||
byte[] group = new byte[4];
|
||||
group[0] = (byte) 239;
|
||||
group[1] = legalAddressByte(b[0]);
|
||||
group[2] = legalAddressByte(b[1]);
|
||||
group[3] = legalAddressByte(b[2]);
|
||||
// The port is random in the range 32768 - 65535, inclusive
|
||||
int port = ByteUtils.readUint16(b, 3);
|
||||
if(port < 32768) port += 32768;
|
||||
InetAddress address;
|
||||
try {
|
||||
address = InetAddress.getByAddress(group);
|
||||
} catch(UnknownHostException badAddressLength) {
|
||||
throw new RuntimeException(badAddressLength);
|
||||
}
|
||||
return new InetSocketAddress(address, port);
|
||||
}
|
||||
|
||||
private byte legalAddressByte(byte b) {
|
||||
if(b == 0) return 1;
|
||||
if(b == (byte) 255) return (byte) 254;
|
||||
return b;
|
||||
}
|
||||
|
||||
private InetAddress chooseInvitationInterface() throws IOException {
|
||||
List<NetworkInterface> ifaces =
|
||||
Collections.list(NetworkInterface.getNetworkInterfaces());
|
||||
// Prefer an interface with a link-local or site-local address
|
||||
for(NetworkInterface iface : ifaces) {
|
||||
for(InetAddress addr : Collections.list(iface.getInetAddresses())) {
|
||||
if(addr.isLoopbackAddress()) continue;
|
||||
boolean link = addr.isLinkLocalAddress();
|
||||
boolean site = addr.isSiteLocalAddress();
|
||||
if(link || site) return addr;
|
||||
}
|
||||
}
|
||||
// Accept an interface without a link-local or site-local address
|
||||
for(NetworkInterface iface : ifaces) {
|
||||
for(InetAddress addr : Collections.list(iface.getInetAddresses())) {
|
||||
if(!addr.isLoopbackAddress()) return addr;
|
||||
}
|
||||
}
|
||||
// No suitable interfaces
|
||||
return null;
|
||||
}
|
||||
|
||||
private void tryToClose(MulticastSocket ms, InetAddress addr) {
|
||||
try {
|
||||
ms.leaveGroup(addr);
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
ms.close();
|
||||
}
|
||||
|
||||
private class MulticastListenerThread extends Thread {
|
||||
|
||||
private final LatchedReference<Socket> socketLatch;
|
||||
private final MulticastSocket multicastSocket;
|
||||
private final InetAddress localAddress;
|
||||
|
||||
private MulticastListenerThread(LatchedReference<Socket> socketLatch,
|
||||
MulticastSocket multicastSocket, InetAddress localAddress) {
|
||||
this.socketLatch = socketLatch;
|
||||
this.multicastSocket = multicastSocket;
|
||||
this.localAddress = localAddress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Listening for multicast packets");
|
||||
// Listen until a valid packet is received or the socket is closed
|
||||
byte[] buffer = new byte[2];
|
||||
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
|
||||
try {
|
||||
while(running) {
|
||||
multicastSocket.receive(packet);
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Received multicast packet");
|
||||
parseAndConnectBack(packet);
|
||||
}
|
||||
} catch(IOException e) {
|
||||
// This is expected when the socket is closed
|
||||
if(LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void parseAndConnectBack(DatagramPacket packet) {
|
||||
InetAddress addr = packet.getAddress();
|
||||
if(addr.equals(localAddress)) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Ignoring own packet");
|
||||
return;
|
||||
}
|
||||
byte[] b = packet.getData();
|
||||
int off = packet.getOffset();
|
||||
int len = packet.getLength();
|
||||
if(len != 2) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Invalid length: " + len);
|
||||
return;
|
||||
}
|
||||
int port = ByteUtils.readUint16(b, off);
|
||||
if(port < 32768 || port >= 65536) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Invalid port: " + port);
|
||||
return;
|
||||
}
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Packet from " + getHostAddress(addr) + ":" + port);
|
||||
try {
|
||||
// Connect back on the advertised TCP port
|
||||
Socket s = new Socket(addr, port);
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Outgoing connection");
|
||||
if(!socketLatch.set(s)) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Closing redundant connection");
|
||||
s.close();
|
||||
}
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TcpListenerThread extends Thread {
|
||||
|
||||
private final LatchedReference<Socket> socketLatch;
|
||||
private final ServerSocket serverSocket;
|
||||
|
||||
private TcpListenerThread(LatchedReference<Socket> socketLatch,
|
||||
ServerSocket serverSocket) {
|
||||
this.socketLatch = socketLatch;
|
||||
this.serverSocket = serverSocket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Listening for invitation connections");
|
||||
// Listen until a connection is received or the socket is closed
|
||||
try {
|
||||
Socket 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,34 @@
|
||||
package org.briarproject.plugins.tcp;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.briarproject.api.TransportId;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPlugin;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
|
||||
import org.briarproject.api.system.Clock;
|
||||
import org.briarproject.api.system.SystemClock;
|
||||
|
||||
public class LanTcpPluginFactory 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 Clock clock;
|
||||
|
||||
public LanTcpPluginFactory(Executor pluginExecutor) {
|
||||
this.pluginExecutor = pluginExecutor;
|
||||
clock = new SystemClock();
|
||||
}
|
||||
|
||||
public TransportId getId() {
|
||||
return LanTcpPlugin.ID;
|
||||
}
|
||||
|
||||
public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
|
||||
return new LanTcpPlugin(pluginExecutor, clock, callback,
|
||||
MAX_FRAME_LENGTH, MAX_LATENCY, POLLING_INTERVAL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.briarproject.plugins.tcp;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
class MappingResult {
|
||||
|
||||
private final InetAddress internal, external;
|
||||
private final int port;
|
||||
private final boolean succeeded;
|
||||
|
||||
MappingResult(InetAddress internal, InetAddress external, int port,
|
||||
boolean succeeded) {
|
||||
this.internal = internal;
|
||||
this.external = external;
|
||||
this.port = port;
|
||||
this.succeeded = succeeded;
|
||||
}
|
||||
|
||||
InetSocketAddress getInternal() {
|
||||
return isUsable() ? new InetSocketAddress(internal, port) : null;
|
||||
}
|
||||
|
||||
InetSocketAddress getExternal() {
|
||||
return isUsable() ? new InetSocketAddress(external, port) : null;
|
||||
}
|
||||
|
||||
boolean isUsable() {
|
||||
return internal != null && external != null && port != 0 && succeeded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.briarproject.plugins.tcp;
|
||||
|
||||
interface PortMapper {
|
||||
|
||||
MappingResult map(int port);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package org.briarproject.plugins.tcp;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import org.briarproject.api.lifecycle.ShutdownManager;
|
||||
|
||||
import org.bitlet.weupnp.GatewayDevice;
|
||||
import org.bitlet.weupnp.GatewayDiscover;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
class PortMapperImpl implements PortMapper {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(PortMapperImpl.class.getName());
|
||||
|
||||
private final ShutdownManager shutdownManager;
|
||||
private final AtomicBoolean started = new AtomicBoolean(false);
|
||||
|
||||
private volatile GatewayDevice gateway = null;
|
||||
|
||||
PortMapperImpl(ShutdownManager shutdownManager) {
|
||||
this.shutdownManager = shutdownManager;
|
||||
}
|
||||
|
||||
public MappingResult map(final int port) {
|
||||
if(!started.getAndSet(true)) start();
|
||||
if(gateway == null) return null;
|
||||
InetAddress internal = gateway.getLocalAddress();
|
||||
if(internal == null) return null;
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Internal address " + getHostAddress(internal));
|
||||
boolean succeeded = false;
|
||||
InetAddress external = null;
|
||||
try {
|
||||
succeeded = gateway.addPortMapping(port, port,
|
||||
getHostAddress(internal), "TCP", "TCP");
|
||||
if(succeeded) {
|
||||
shutdownManager.addShutdownHook(new Runnable() {
|
||||
public void run() {
|
||||
deleteMapping(port);
|
||||
}
|
||||
});
|
||||
}
|
||||
String externalString = gateway.getExternalIPAddress();
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("External address " + externalString);
|
||||
if(externalString != null)
|
||||
external = InetAddress.getByName(externalString);
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
} catch(SAXException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
return new MappingResult(internal, external, port, succeeded);
|
||||
}
|
||||
|
||||
private String getHostAddress(InetAddress a) {
|
||||
String addr = a.getHostAddress();
|
||||
int percent = addr.indexOf('%');
|
||||
if(percent == -1) return addr;
|
||||
return addr.substring(0, percent);
|
||||
}
|
||||
|
||||
private void start() {
|
||||
GatewayDiscover d = new GatewayDiscover();
|
||||
try {
|
||||
d.discover();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
} catch(SAXException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
} catch(ParserConfigurationException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
gateway = d.getValidGateway();
|
||||
}
|
||||
|
||||
private void deleteMapping(int port) {
|
||||
try {
|
||||
gateway.deletePortMapping(port, "TCP");
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Deleted mapping for port " + port);
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
} catch(SAXException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
210
briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java
Normal file
210
briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java
Normal file
@@ -0,0 +1,210 @@
|
||||
package org.briarproject.plugins.tcp;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.briarproject.api.ContactId;
|
||||
import org.briarproject.api.TransportProperties;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPlugin;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
|
||||
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
|
||||
import org.briarproject.util.StringUtils;
|
||||
|
||||
abstract class TcpPlugin implements DuplexPlugin {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(TcpPlugin.class.getName());
|
||||
|
||||
protected final Executor pluginExecutor;
|
||||
protected final DuplexPluginCallback callback;
|
||||
protected final int maxFrameLength;
|
||||
protected final long maxLatency, pollingInterval;
|
||||
|
||||
protected volatile boolean running = false;
|
||||
private volatile ServerSocket socket = null;
|
||||
|
||||
/**
|
||||
* Returns zero or more socket addresses on which the plugin should listen,
|
||||
* in order of preference. At most one of the addresses will be bound.
|
||||
*/
|
||||
protected abstract List<SocketAddress> getLocalSocketAddresses();
|
||||
|
||||
protected TcpPlugin(Executor pluginExecutor, DuplexPluginCallback callback,
|
||||
int maxFrameLength, long maxLatency, long pollingInterval) {
|
||||
this.pluginExecutor = pluginExecutor;
|
||||
this.callback = callback;
|
||||
this.maxFrameLength = maxFrameLength;
|
||||
this.maxLatency = maxLatency;
|
||||
this.pollingInterval = pollingInterval;
|
||||
}
|
||||
|
||||
public int getMaxFrameLength() {
|
||||
return maxFrameLength;
|
||||
}
|
||||
|
||||
public long getMaxLatency() {
|
||||
return maxLatency;
|
||||
}
|
||||
|
||||
public boolean start() {
|
||||
running = true;
|
||||
pluginExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
bind();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private void bind() {
|
||||
ServerSocket ss = null;
|
||||
boolean found = false;
|
||||
for(SocketAddress addr : getLocalSocketAddresses()) {
|
||||
try {
|
||||
ss = new ServerSocket();
|
||||
ss.bind(addr);
|
||||
found = true;
|
||||
break;
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Failed to bind " + addr);
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
tryToClose(ss);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if(!found) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Could not bind server socket");
|
||||
return;
|
||||
}
|
||||
if(!running) {
|
||||
tryToClose(ss);
|
||||
return;
|
||||
}
|
||||
socket = ss;
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Listening on " + ss.getLocalSocketAddress());
|
||||
setLocalSocketAddress((InetSocketAddress) ss.getLocalSocketAddress());
|
||||
acceptContactConnections(ss);
|
||||
}
|
||||
|
||||
protected void tryToClose(ServerSocket ss) {
|
||||
try {
|
||||
ss.close();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
protected String getHostAddress(InetAddress a) {
|
||||
String addr = a.getHostAddress();
|
||||
int percent = addr.indexOf('%');
|
||||
return percent == -1 ? addr : addr.substring(0, percent);
|
||||
}
|
||||
|
||||
protected void setLocalSocketAddress(InetSocketAddress a) {
|
||||
TransportProperties p = new TransportProperties();
|
||||
p.put("address", getHostAddress(a.getAddress()));
|
||||
p.put("port", String.valueOf(a.getPort()));
|
||||
callback.mergeLocalProperties(p);
|
||||
}
|
||||
|
||||
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 from " + s.getRemoteSocketAddress());
|
||||
TcpTransportConnection conn = new TcpTransportConnection(this, s);
|
||||
callback.incomingConnectionCreated(conn);
|
||||
if(!running) return;
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
running = false;
|
||||
if(socket != null) tryToClose(socket);
|
||||
}
|
||||
|
||||
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;
|
||||
SocketAddress addr = getRemoteSocketAddress(c);
|
||||
if(addr == null) return null;
|
||||
Socket s = new Socket();
|
||||
try {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Connecting to " + addr);
|
||||
s.connect(addr);
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Connected to " + addr);
|
||||
return new TcpTransportConnection(this, s);
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private SocketAddress getRemoteSocketAddress(ContactId c) {
|
||||
TransportProperties p = callback.getRemoteProperties().get(c);
|
||||
if(p == null) return null;
|
||||
String addrString = p.get("address");
|
||||
if(StringUtils.isNullOrEmpty(addrString)) return null;
|
||||
String portString = p.get("port");
|
||||
if(StringUtils.isNullOrEmpty(portString)) return null;
|
||||
try {
|
||||
InetAddress addr = InetAddress.getByName(addrString);
|
||||
int port = Integer.parseInt(portString);
|
||||
return new InetSocketAddress(addr, port);
|
||||
} catch(NumberFormatException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return null;
|
||||
} catch(UnknownHostException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.briarproject.plugins.tcp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
|
||||
import org.briarproject.api.plugins.Plugin;
|
||||
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
|
||||
|
||||
class TcpTransportConnection implements DuplexTransportConnection {
|
||||
|
||||
private final Plugin plugin;
|
||||
private final Socket socket;
|
||||
|
||||
TcpTransportConnection(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();
|
||||
}
|
||||
}
|
||||
132
briar-core/src/org/briarproject/plugins/tcp/WanTcpPlugin.java
Normal file
132
briar-core/src/org/briarproject/plugins/tcp/WanTcpPlugin.java
Normal file
@@ -0,0 +1,132 @@
|
||||
package org.briarproject.plugins.tcp;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.briarproject.api.TransportId;
|
||||
import org.briarproject.api.TransportProperties;
|
||||
import org.briarproject.api.crypto.PseudoRandom;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
|
||||
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
|
||||
import org.briarproject.util.StringUtils;
|
||||
|
||||
class WanTcpPlugin extends TcpPlugin {
|
||||
|
||||
static final byte[] TRANSPORT_ID =
|
||||
StringUtils.fromHexString("58c66d999e492b85065924acfd739d80"
|
||||
+ "c65a62f87e5a4fc6c284f95908b9007d"
|
||||
+ "512a93ebf89bf68f50a29e96eebf97b6");
|
||||
static final TransportId ID = new TransportId(TRANSPORT_ID);
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(WanTcpPlugin.class.getName());
|
||||
|
||||
private final PortMapper portMapper;
|
||||
|
||||
private volatile MappingResult mappingResult;
|
||||
|
||||
WanTcpPlugin(Executor pluginExecutor, DuplexPluginCallback callback,
|
||||
int maxFrameLength, long maxLatency, long pollingInterval,
|
||||
PortMapper portMapper) {
|
||||
super(pluginExecutor, callback, maxFrameLength, maxLatency,
|
||||
pollingInterval);
|
||||
this.portMapper = portMapper;
|
||||
}
|
||||
|
||||
public TransportId getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return "WAN_TCP_PLUGIN_NAME";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<SocketAddress> getLocalSocketAddresses() {
|
||||
List<SocketAddress> addrs = new ArrayList<SocketAddress>();
|
||||
// Prefer a previously used address and port if available
|
||||
TransportProperties p = callback.getLocalProperties();
|
||||
String addrString = p.get("address");
|
||||
String portString = p.get("port");
|
||||
InetAddress addr = null;
|
||||
int port = 0;
|
||||
if(!StringUtils.isNullOrEmpty(addrString) &&
|
||||
!StringUtils.isNullOrEmpty(portString)) {
|
||||
try {
|
||||
addr = InetAddress.getByName(addrString);
|
||||
port = Integer.parseInt(portString);
|
||||
addrs.add(new InetSocketAddress(addr, port));
|
||||
addrs.add(new InetSocketAddress(addr, 0));
|
||||
} catch(NumberFormatException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
} catch(UnknownHostException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
// Get a list of the device's network interfaces
|
||||
List<NetworkInterface> ifaces;
|
||||
try {
|
||||
ifaces = Collections.list(NetworkInterface.getNetworkInterfaces());
|
||||
} catch(SocketException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return addrs;
|
||||
}
|
||||
// Accept interfaces without link-local or site-local addresses
|
||||
for(NetworkInterface iface : ifaces) {
|
||||
for(InetAddress a : Collections.list(iface.getInetAddresses())) {
|
||||
if(addr != null && a.equals(addr)) continue;
|
||||
if(a instanceof Inet6Address) continue;
|
||||
if(a.isLoopbackAddress()) continue;
|
||||
boolean link = a.isLinkLocalAddress();
|
||||
boolean site = a.isSiteLocalAddress();
|
||||
if(!link && !site) addrs.add(new InetSocketAddress(a, 0));
|
||||
}
|
||||
}
|
||||
// Accept interfaces with local addresses that can be port-mapped
|
||||
if(port == 0) port = chooseEphemeralPort();
|
||||
mappingResult = portMapper.map(port);
|
||||
if(mappingResult != null && mappingResult.isUsable()) {
|
||||
InetSocketAddress a = mappingResult.getInternal();
|
||||
if(!(a.getAddress() instanceof Inet6Address)) addrs.add(a);
|
||||
}
|
||||
return addrs;
|
||||
}
|
||||
|
||||
private int chooseEphemeralPort() {
|
||||
return 32768 + (int) (Math.random() * 32768);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setLocalSocketAddress(InetSocketAddress a) {
|
||||
if(mappingResult != null && mappingResult.isUsable()) {
|
||||
// Advertise the external address to contacts
|
||||
if(a.equals(mappingResult.getInternal()))
|
||||
a = mappingResult.getExternal();
|
||||
}
|
||||
TransportProperties p = new TransportProperties();
|
||||
p.put("address", getHostAddress(a.getAddress()));
|
||||
p.put("port", String.valueOf(a.getPort()));
|
||||
callback.mergeLocalProperties(p);
|
||||
}
|
||||
|
||||
public boolean supportsInvitations() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
|
||||
long timeout) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.briarproject.plugins.tcp;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import org.briarproject.api.TransportId;
|
||||
import org.briarproject.api.lifecycle.ShutdownManager;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPlugin;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
|
||||
import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
|
||||
|
||||
public class WanTcpPluginFactory 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 = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
private final Executor pluginExecutor;
|
||||
private final ShutdownManager shutdownManager;
|
||||
|
||||
public WanTcpPluginFactory(Executor pluginExecutor,
|
||||
ShutdownManager shutdownManager) {
|
||||
this.pluginExecutor = pluginExecutor;
|
||||
this.shutdownManager = shutdownManager;
|
||||
}
|
||||
|
||||
public TransportId getId() {
|
||||
return WanTcpPlugin.ID;
|
||||
}
|
||||
|
||||
public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
|
||||
return new WanTcpPlugin(pluginExecutor, callback, MAX_FRAME_LENGTH,
|
||||
MAX_LATENCY, POLLING_INTERVAL,
|
||||
new PortMapperImpl(shutdownManager));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user