diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/connection/ConnectionManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/connection/ConnectionManager.java index e08a73e9b..3d7df9bea 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/connection/ConnectionManager.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/connection/ConnectionManager.java @@ -16,6 +16,17 @@ public interface ConnectionManager { */ void manageIncomingConnection(TransportId t, TransportConnectionReader r); + /** + * Manages an incoming connection from a contact via a mailbox. + *

+ * This method does not mark the tag as recognised until after the data + * has been read from the {@link TransportConnectionReader}, at which + * point the {@link TagController} is called to decide whether the tag + * should be marked as recognised. + */ + void manageIncomingConnection(TransportId t, TransportConnectionReader r, + TagController c); + /** * Manages an incoming connection from a contact over a duplex transport. */ @@ -46,4 +57,21 @@ public interface ConnectionManager { */ void manageOutgoingConnection(PendingContactId p, TransportId t, DuplexTransportConnection d); + + /** + * An interface for controlling whether a tag should be marked as + * recognised. + */ + interface TagController { + /** + * This method is only called if a tag was read from the corresponding + * {@link TransportConnectionReader} and recognised. + * + * @param exception True if an exception was thrown while reading from + * the {@link TransportConnectionReader}, after successfully reading + * and recognising the tag. + * @return True if the tag should be marked as recognised. + */ + boolean shouldMarkTagAsRecognised(boolean exception); + } } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxConstants.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxConstants.java index 8cc6b5fc0..51d7f0a7c 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxConstants.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxConstants.java @@ -1,5 +1,7 @@ package org.briarproject.bramble.api.mailbox; +import org.briarproject.bramble.api.plugin.TransportId; + import static java.util.concurrent.TimeUnit.HOURS; import static org.briarproject.bramble.api.transport.TransportConstants.MAX_FRAME_LENGTH; import static org.briarproject.bramble.api.transport.TransportConstants.MAX_PAYLOAD_LENGTH; @@ -8,6 +10,11 @@ import static org.briarproject.bramble.api.transport.TransportConstants.TAG_LENG public interface MailboxConstants { + /** + * The transport ID of the mailbox plugin. + */ + TransportId ID = new TransportId("org.briarproject.bramble.mailbox"); + /** * The maximum length of a file that can be uploaded to or downloaded from * a mailbox. diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxDirectory.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxDirectory.java new file mode 100644 index 000000000..28151dd15 --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxDirectory.java @@ -0,0 +1,22 @@ +package org.briarproject.bramble.api.mailbox; + +import java.io.File; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotation for injecting the {@link File directory} where the Mailbox plugin + * should store its state. + */ +@Qualifier +@Target({FIELD, METHOD, PARAMETER}) +@Retention(RUNTIME) +public @interface MailboxDirectory { +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/connection/Connection.java b/bramble-core/src/main/java/org/briarproject/bramble/connection/Connection.java index 9ca5c672e..9db61721c 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/connection/Connection.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/connection/Connection.java @@ -54,7 +54,7 @@ abstract class Connection { } } - private byte[] readTag(InputStream in) throws IOException { + byte[] readTag(InputStream in) throws IOException { byte[] tag = new byte[TAG_LENGTH]; read(in, tag); return tag; diff --git a/bramble-core/src/main/java/org/briarproject/bramble/connection/ConnectionManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/connection/ConnectionManagerImpl.java index 2a50033b1..2e37ce7cd 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/connection/ConnectionManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/connection/ConnectionManagerImpl.java @@ -67,7 +67,15 @@ class ConnectionManagerImpl implements ConnectionManager { TransportConnectionReader r) { ioExecutor.execute(new IncomingSimplexSyncConnection(keyManager, connectionRegistry, streamReaderFactory, streamWriterFactory, - syncSessionFactory, transportPropertyManager, t, r)); + syncSessionFactory, transportPropertyManager, t, r, null)); + } + + @Override + public void manageIncomingConnection(TransportId t, + TransportConnectionReader r, TagController c) { + ioExecutor.execute(new IncomingSimplexSyncConnection(keyManager, + connectionRegistry, streamReaderFactory, streamWriterFactory, + syncSessionFactory, transportPropertyManager, t, r, c)); } @Override diff --git a/bramble-core/src/main/java/org/briarproject/bramble/connection/IncomingSimplexSyncConnection.java b/bramble-core/src/main/java/org/briarproject/bramble/connection/IncomingSimplexSyncConnection.java index b41fb33e0..68b9d969c 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/connection/IncomingSimplexSyncConnection.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/connection/IncomingSimplexSyncConnection.java @@ -1,7 +1,9 @@ package org.briarproject.bramble.connection; +import org.briarproject.bramble.api.connection.ConnectionManager.TagController; import org.briarproject.bramble.api.connection.ConnectionRegistry; import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.plugin.TransportConnectionReader; import org.briarproject.bramble.api.plugin.TransportId; @@ -15,6 +17,8 @@ import org.briarproject.bramble.api.transport.StreamWriterFactory; import java.io.IOException; +import javax.annotation.Nullable; + import static java.util.logging.Level.WARNING; import static org.briarproject.bramble.util.LogUtils.logException; @@ -23,6 +27,8 @@ class IncomingSimplexSyncConnection extends SyncConnection implements Runnable { private final TransportId transportId; private final TransportConnectionReader reader; + @Nullable + private final TagController tagController; IncomingSimplexSyncConnection(KeyManager keyManager, ConnectionRegistry connectionRegistry, @@ -30,33 +36,50 @@ class IncomingSimplexSyncConnection extends SyncConnection implements Runnable { StreamWriterFactory streamWriterFactory, SyncSessionFactory syncSessionFactory, TransportPropertyManager transportPropertyManager, - TransportId transportId, TransportConnectionReader reader) { + TransportId transportId, + TransportConnectionReader reader, + @Nullable TagController tagController) { super(keyManager, connectionRegistry, streamReaderFactory, streamWriterFactory, syncSessionFactory, transportPropertyManager); this.transportId = transportId; this.reader = reader; + this.tagController = tagController; } @Override public void run() { // Read and recognise the tag - StreamContext ctx = recogniseTag(reader, transportId); + byte[] tag; + StreamContext ctx; + try { + tag = readTag(reader.getInputStream()); + // If we have a tag controller, defer marking the tag as recognised + if (tagController == null) { + ctx = keyManager.getStreamContext(transportId, tag); + } else { + ctx = keyManager.getStreamContextOnly(transportId, tag); + } + } catch (IOException | DbException e) { + logException(LOG, WARNING, e); + onError(); + return; + } if (ctx == null) { LOG.info("Unrecognised tag"); - onError(false); + onError(); return; } ContactId contactId = ctx.getContactId(); if (contactId == null) { LOG.warning("Received rendezvous stream, expected contact"); - onError(true); + onError(tag); return; } if (ctx.isHandshakeMode()) { // TODO: Support handshake mode for contacts LOG.warning("Received handshake tag, expected rotation mode"); - onError(true); + onError(tag); return; } try { @@ -65,15 +88,33 @@ class IncomingSimplexSyncConnection extends SyncConnection implements Runnable { LOG.info("Ignoring priority for simplex connection"); // Create and run the incoming session createIncomingSession(ctx, reader, handler).run(); + // Success + markTagAsRecognisedIfRequired(false, tag); reader.dispose(false, true); } catch (IOException e) { logException(LOG, WARNING, e); - onError(true); + onError(tag); } } - private void onError(boolean recognised) { - disposeOnError(reader, recognised); + private void onError() { + disposeOnError(reader, false); + } + + private void onError(byte[] tag) { + markTagAsRecognisedIfRequired(true, tag); + disposeOnError(reader, true); + } + + private void markTagAsRecognisedIfRequired(boolean exception, byte[] tag) { + if (tagController != null && + tagController.shouldMarkTagAsRecognised(exception)) { + try { + keyManager.markTagAsRecognised(transportId, tag); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + } } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxFileManager.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxFileManager.java new file mode 100644 index 000000000..0320b1bd5 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxFileManager.java @@ -0,0 +1,24 @@ +package org.briarproject.bramble.mailbox; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import java.io.File; +import java.io.IOException; + +import javax.annotation.concurrent.ThreadSafe; + +@ThreadSafe +@NotNullByDefault +interface MailboxFileManager { + + /** + * Creates an empty file for storing a download. + */ + File createTempFileForDownload() throws IOException; + + /** + * Handles a file that has been downloaded. The file should be created + * with {@link #createTempFileForDownload()}. + */ + void handleDownloadedFile(File f); +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxFileManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxFileManagerImpl.java new file mode 100644 index 000000000..b3d84837c --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxFileManagerImpl.java @@ -0,0 +1,174 @@ +package org.briarproject.bramble.mailbox; + +import org.briarproject.bramble.api.connection.ConnectionManager; +import org.briarproject.bramble.api.event.Event; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.event.EventListener; +import org.briarproject.bramble.api.lifecycle.IoExecutor; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.mailbox.MailboxDirectory; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.PluginManager; +import org.briarproject.bramble.api.plugin.TransportConnectionReader; +import org.briarproject.bramble.api.plugin.event.TransportActiveEvent; +import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin; +import org.briarproject.bramble.api.properties.TransportProperties; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.annotation.concurrent.ThreadSafe; +import javax.inject.Inject; + +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING; +import static org.briarproject.bramble.api.mailbox.MailboxConstants.ID; +import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; +import static org.briarproject.bramble.api.plugin.file.FileConstants.PROP_PATH; +import static org.briarproject.bramble.util.LogUtils.logException; + +@ThreadSafe +@NotNullByDefault +class MailboxFileManagerImpl implements MailboxFileManager, EventListener { + + private static final Logger LOG = + getLogger(MailboxFileManagerImpl.class.getName()); + + // Package access for testing + static final String DOWNLOAD_DIR_NAME = "downloads"; + + private final Executor ioExecutor; + private final PluginManager pluginManager; + private final ConnectionManager connectionManager; + private final LifecycleManager lifecycleManager; + private final File mailboxDir; + private final EventBus eventBus; + private final CountDownLatch orphanLatch = new CountDownLatch(1); + + @Inject + MailboxFileManagerImpl(@IoExecutor Executor ioExecutor, + PluginManager pluginManager, + ConnectionManager connectionManager, + LifecycleManager lifecycleManager, + @MailboxDirectory File mailboxDir, + EventBus eventBus) { + this.ioExecutor = ioExecutor; + this.pluginManager = pluginManager; + this.connectionManager = connectionManager; + this.lifecycleManager = lifecycleManager; + this.mailboxDir = mailboxDir; + this.eventBus = eventBus; + } + + @Override + public File createTempFileForDownload() throws IOException { + // Wait for orphaned files to be handled before creating new files + try { + orphanLatch.await(); + } catch (InterruptedException e) { + throw new IOException(e); + } + File downloadDir = createDirectoryIfNeeded(DOWNLOAD_DIR_NAME); + return File.createTempFile("mailbox", ".tmp", downloadDir); + } + + private File createDirectoryIfNeeded(String name) throws IOException { + File dir = new File(mailboxDir, name); + //noinspection ResultOfMethodCallIgnored + dir.mkdirs(); + if (!dir.isDirectory()) { + throw new IOException("Failed to create directory '" + name + "'"); + } + return dir; + } + + @Override + public void handleDownloadedFile(File f) { + // We shouldn't reach this point until the plugin has been started + SimplexPlugin plugin = + (SimplexPlugin) requireNonNull(pluginManager.getPlugin(ID)); + TransportProperties p = new TransportProperties(); + p.put(PROP_PATH, f.getAbsolutePath()); + TransportConnectionReader reader = plugin.createReader(p); + if (reader == null) { + LOG.warning("Failed to create reader for downloaded file"); + return; + } + TransportConnectionReader decorated = new MailboxFileReader(reader, f); + LOG.info("Reading downloaded file"); + connectionManager.manageIncomingConnection(ID, decorated, + exception -> isHandlingComplete(exception, true)); + } + + private boolean isHandlingComplete(boolean exception, boolean recognised) { + // If we've successfully read the file then we're done + if (!exception && recognised) return true; + // If the app is shutting down we may get spurious IO exceptions + // due to executors being shut down. Leave the file in the download + // directory and we'll try to read it again at the next startup + return !lifecycleManager.getLifecycleState().isAfter(RUNNING); + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof TransportActiveEvent) { + TransportActiveEvent t = (TransportActiveEvent) e; + if (t.getTransportId().equals(ID)) { + ioExecutor.execute(this::handleOrphanedFiles); + eventBus.removeListener(this); + } + } + } + + /** + * This method is called at startup, as soon as the plugin is started, to + * handle any files that were left in the download directory at the last + * shutdown. + */ + @IoExecutor + private void handleOrphanedFiles() { + try { + File downloadDir = createDirectoryIfNeeded(DOWNLOAD_DIR_NAME); + File[] orphans = downloadDir.listFiles(); + // Now that we've got the list of orphans, new files can be created + orphanLatch.countDown(); + if (orphans != null) for (File f : orphans) handleDownloadedFile(f); + } catch (IOException e) { + logException(LOG, WARNING, e); + } + } + + private class MailboxFileReader implements TransportConnectionReader { + + private final TransportConnectionReader delegate; + private final File file; + + private MailboxFileReader(TransportConnectionReader delegate, + File file) { + this.delegate = delegate; + this.file = file; + } + + @Override + public InputStream getInputStream() throws IOException { + return delegate.getInputStream(); + } + + @Override + public void dispose(boolean exception, boolean recognised) + throws IOException { + delegate.dispose(exception, recognised); + if (isHandlingComplete(exception, recognised)) { + LOG.info("Deleting downloaded file"); + if (!file.delete()) { + LOG.warning("Failed to delete downloaded file"); + } + } + } + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxModule.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxModule.java index 590a7f857..3a675f774 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxModule.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxModule.java @@ -4,6 +4,7 @@ import org.briarproject.bramble.api.FeatureFlags; import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.data.MetadataEncoder; +import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.mailbox.MailboxManager; import org.briarproject.bramble.api.mailbox.MailboxSettingsManager; @@ -34,6 +35,8 @@ public class MailboxModule { MailboxUpdateValidator mailboxUpdateValidator; @Inject MailboxUpdateManager mailboxUpdateManager; + @Inject + MailboxFileManager mailboxFileManager; } @Provides @@ -101,4 +104,14 @@ public class MailboxModule { } return mailboxUpdateManager; } + + @Provides + @Singleton + MailboxFileManager provideMailboxFileManager(FeatureFlags featureFlags, + EventBus eventBus, MailboxFileManagerImpl mailboxFileManager) { + if (featureFlags.shouldEnableMailbox()) { + eventBus.addListener(mailboxFileManager); + } + return mailboxFileManager; + } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/AbstractRemovableDrivePlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/AbstractRemovableDrivePlugin.java index ff4b9c048..ccdf7f56b 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/AbstractRemovableDrivePlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/AbstractRemovableDrivePlugin.java @@ -67,6 +67,7 @@ abstract class AbstractRemovableDrivePlugin implements SimplexPlugin { public void start() { callback.mergeLocalProperties( new TransportProperties(singletonMap(PROP_SUPPORTED, "true"))); + callback.pluginStateChanged(ACTIVE); } @Override diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FilePlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FilePlugin.java index 69b368a55..54587dc9c 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FilePlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FilePlugin.java @@ -11,6 +11,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.util.logging.Logger; import static java.util.logging.Level.WARNING; @@ -29,11 +30,6 @@ abstract class FilePlugin implements SimplexPlugin { protected final PluginCallback callback; protected final long maxLatency; - protected abstract void writerFinished(File f, boolean exception); - - protected abstract void readerFinished(File f, boolean exception, - boolean recognised); - FilePlugin(PluginCallback callback, long maxLatency) { this.callback = callback; this.maxLatency = maxLatency; @@ -50,9 +46,8 @@ abstract class FilePlugin implements SimplexPlugin { String path = p.get(PROP_PATH); if (isNullOrEmpty(path)) return null; try { - File file = new File(path); - FileInputStream in = new FileInputStream(file); - return new FileTransportReader(file, in, this); + FileInputStream in = new FileInputStream(path); + return new TransportInputStreamReader(in); } catch (IOException e) { logException(LOG, WARNING, e); return null; @@ -70,8 +65,8 @@ abstract class FilePlugin implements SimplexPlugin { LOG.info("Failed to create file"); return null; } - FileOutputStream out = new FileOutputStream(file); - return new FileTransportWriter(file, out, this); + OutputStream out = new FileOutputStream(file); + return new TransportOutputStreamWriter(this, out); } catch (IOException e) { logException(LOG, WARNING, e); return null; diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FileTransportReader.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FileTransportReader.java deleted file mode 100644 index 07a84d294..000000000 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FileTransportReader.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.briarproject.bramble.plugin.file; - -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.plugin.TransportConnectionReader; - -import java.io.File; -import java.io.InputStream; -import java.util.logging.Logger; - -import static java.util.logging.Level.WARNING; -import static org.briarproject.bramble.util.IoUtils.tryToClose; - -@NotNullByDefault -class FileTransportReader implements TransportConnectionReader { - - 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; - } - - @Override - public InputStream getInputStream() { - return in; - } - - @Override - public void dispose(boolean exception, boolean recognised) { - tryToClose(in, LOG, WARNING); - plugin.readerFinished(file, exception, recognised); - } -} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FileTransportWriter.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FileTransportWriter.java deleted file mode 100644 index 3dc6fc428..000000000 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FileTransportWriter.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.briarproject.bramble.plugin.file; - -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.plugin.TransportConnectionWriter; - -import java.io.File; -import java.io.OutputStream; -import java.util.logging.Logger; - -import static java.util.logging.Level.WARNING; -import static org.briarproject.bramble.util.IoUtils.tryToClose; - -@NotNullByDefault -class FileTransportWriter implements TransportConnectionWriter { - - private static final Logger LOG = - Logger.getLogger(FileTransportWriter.class.getName()); - - private final File file; - private final OutputStream out; - private final FilePlugin plugin; - - FileTransportWriter(File file, OutputStream out, FilePlugin plugin) { - this.file = file; - this.out = out; - this.plugin = plugin; - } - - @Override - public long getMaxLatency() { - return plugin.getMaxLatency(); - } - - @Override - public int getMaxIdleTime() { - return plugin.getMaxIdleTime(); - } - - @Override - public boolean isLossyAndCheap() { - return plugin.isLossyAndCheap(); - } - - @Override - public OutputStream getOutputStream() { - return out; - } - - @Override - public void dispose(boolean exception) { - tryToClose(out, LOG, WARNING); - plugin.writerFinished(file, exception); - } -} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/MailboxPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/MailboxPlugin.java new file mode 100644 index 000000000..a70e65708 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/MailboxPlugin.java @@ -0,0 +1,73 @@ +package org.briarproject.bramble.plugin.file; + +import org.briarproject.bramble.api.Pair; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.ConnectionHandler; +import org.briarproject.bramble.api.plugin.PluginCallback; +import org.briarproject.bramble.api.plugin.PluginException; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.properties.TransportProperties; + +import java.util.Collection; + +import static org.briarproject.bramble.api.mailbox.MailboxConstants.ID; +import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE; + +@NotNullByDefault +class MailboxPlugin extends FilePlugin { + + MailboxPlugin(PluginCallback callback, long maxLatency) { + super(callback, maxLatency); + } + + @Override + public TransportId getId() { + return ID; + } + + @Override + public int getMaxIdleTime() { + // Unused for simplex transports + throw new UnsupportedOperationException(); + } + + @Override + public void start() throws PluginException { + callback.pluginStateChanged(ACTIVE); + } + + @Override + public void stop() throws PluginException { + } + + @Override + public State getState() { + return ACTIVE; + } + + @Override + public int getReasonsDisabled() { + return 0; + } + + @Override + public boolean shouldPoll() { + return false; + } + + @Override + public int getPollingInterval() { + throw new UnsupportedOperationException(); + } + + @Override + public void poll( + Collection> properties) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isLossyAndCheap() { + return false; + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/MailboxPluginFactory.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/MailboxPluginFactory.java new file mode 100644 index 000000000..98bf72441 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/MailboxPluginFactory.java @@ -0,0 +1,39 @@ +package org.briarproject.bramble.plugin.file; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.PluginCallback; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin; +import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import static java.util.concurrent.TimeUnit.DAYS; +import static org.briarproject.bramble.api.mailbox.MailboxConstants.ID; + +@NotNullByDefault +public class MailboxPluginFactory implements SimplexPluginFactory { + + private static final long MAX_LATENCY = DAYS.toMillis(14); + + @Inject + MailboxPluginFactory() { + } + + @Override + public TransportId getId() { + return ID; + } + + @Override + public long getMaxLatency() { + return MAX_LATENCY; + } + + @Nullable + @Override + public SimplexPlugin createPlugin(PluginCallback callback) { + return new MailboxPlugin(callback, MAX_LATENCY); + } +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxFileManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxFileManagerImplTest.java new file mode 100644 index 000000000..c67ca2dd4 --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxFileManagerImplTest.java @@ -0,0 +1,194 @@ +package org.briarproject.bramble.mailbox; + +import org.briarproject.bramble.api.connection.ConnectionManager; +import org.briarproject.bramble.api.connection.ConnectionManager.TagController; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState; +import org.briarproject.bramble.api.plugin.PluginManager; +import org.briarproject.bramble.api.plugin.TransportConnectionReader; +import org.briarproject.bramble.api.plugin.event.TransportActiveEvent; +import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.test.BrambleMockTestCase; +import org.briarproject.bramble.test.CaptureArgumentAction; +import org.briarproject.bramble.test.RunAction; +import org.jmock.Expectations; +import org.jmock.lib.action.DoAllAction; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; + +import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING; +import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING; +import static org.briarproject.bramble.api.mailbox.MailboxConstants.ID; +import static org.briarproject.bramble.api.plugin.file.FileConstants.PROP_PATH; +import static org.briarproject.bramble.mailbox.MailboxFileManagerImpl.DOWNLOAD_DIR_NAME; +import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory; +import static org.briarproject.bramble.test.TestUtils.getTestDirectory; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class MailboxFileManagerImplTest extends BrambleMockTestCase { + + private final Executor ioExecutor = context.mock(Executor.class); + private final PluginManager pluginManager = + context.mock(PluginManager.class); + private final ConnectionManager connectionManager = + context.mock(ConnectionManager.class); + private final LifecycleManager lifecycleManager = + context.mock(LifecycleManager.class); + private final EventBus eventBus = context.mock(EventBus.class); + private final SimplexPlugin plugin = context.mock(SimplexPlugin.class); + private final TransportConnectionReader transportConnectionReader = + context.mock(TransportConnectionReader.class); + + private File mailboxDir; + private MailboxFileManagerImpl manager; + + @Before + public void setUp() { + mailboxDir = getTestDirectory(); + manager = new MailboxFileManagerImpl(ioExecutor, pluginManager, + connectionManager, lifecycleManager, mailboxDir, eventBus); + } + + @After + public void tearDown() { + deleteTestDirectory(mailboxDir); + } + + @Test + public void testHandlesOrphanedFilesAtStartup() throws Exception { + // Create an orphaned file, left behind at the previous shutdown + File downloadDir = new File(mailboxDir, DOWNLOAD_DIR_NAME); + //noinspection ResultOfMethodCallIgnored + downloadDir.mkdirs(); + File orphan = new File(downloadDir, "orphan"); + assertTrue(orphan.createNewFile()); + + TransportProperties props = new TransportProperties(); + props.put(PROP_PATH, orphan.getAbsolutePath()); + + // When the plugin becomes active the orphaned file should be handled + context.checking(new Expectations() {{ + oneOf(ioExecutor).execute(with(any(Runnable.class))); + will(new RunAction()); + oneOf(eventBus).removeListener(manager); + oneOf(pluginManager).getPlugin(ID); + will(returnValue(plugin)); + oneOf(plugin).createReader(props); + will(returnValue(transportConnectionReader)); + oneOf(connectionManager).manageIncomingConnection(with(ID), + with(any(TransportConnectionReader.class)), + with(any(TagController.class))); + }}); + + manager.eventOccurred(new TransportActiveEvent(ID)); + } + + @Test + public void testDeletesFileWhenReadSucceeds() throws Exception { + expectCheckForOrphans(); + manager.eventOccurred(new TransportActiveEvent(ID)); + + File f = manager.createTempFileForDownload(); + AtomicReference reader = + new AtomicReference<>(null); + AtomicReference controller = new AtomicReference<>(null); + + expectPassFileToConnectionManager(f, reader, controller); + manager.handleDownloadedFile(f); + + // The read is successful, so the tag controller should allow the tag + // to be marked as read and the reader should delete the file + context.checking(new Expectations() {{ + oneOf(transportConnectionReader).dispose(false, true); + }}); + + assertTrue(controller.get().shouldMarkTagAsRecognised(false)); + reader.get().dispose(false, true); + assertFalse(f.exists()); + } + + @Test + public void testDeletesFileWhenTagIsNotRecognised() throws Exception { + testDeletesFile(false, RUNNING, false); + } + + @Test + public void testDeletesFileWhenReadFails() throws Exception { + testDeletesFile(true, RUNNING, false); + } + + @Test + public void testDoesNotDeleteFileWhenTagIsNotRecognisedAtShutdown() + throws Exception { + testDeletesFile(false, STOPPING, true); + } + + @Test + public void testDoesNotDeleteFileWhenReadFailsAtShutdown() + throws Exception { + testDeletesFile(true, STOPPING, true); + } + + private void testDeletesFile(boolean recognised, LifecycleState state, + boolean fileExists) throws Exception { + expectCheckForOrphans(); + manager.eventOccurred(new TransportActiveEvent(ID)); + + File f = manager.createTempFileForDownload(); + AtomicReference reader = + new AtomicReference<>(null); + AtomicReference controller = new AtomicReference<>(null); + + expectPassFileToConnectionManager(f, reader, controller); + manager.handleDownloadedFile(f); + + context.checking(new Expectations() {{ + oneOf(transportConnectionReader).dispose(true, recognised); + oneOf(lifecycleManager).getLifecycleState(); + will(returnValue(state)); + }}); + + reader.get().dispose(true, recognised); + assertEquals(fileExists, f.exists()); + } + + private void expectCheckForOrphans() { + context.checking(new Expectations() {{ + oneOf(ioExecutor).execute(with(any(Runnable.class))); + will(new RunAction()); + oneOf(eventBus).removeListener(manager); + }}); + } + + private void expectPassFileToConnectionManager(File f, + AtomicReference reader, + AtomicReference controller) { + TransportProperties props = new TransportProperties(); + props.put(PROP_PATH, f.getAbsolutePath()); + + context.checking(new Expectations() {{ + oneOf(pluginManager).getPlugin(ID); + will(returnValue(plugin)); + oneOf(plugin).createReader(props); + will(returnValue(transportConnectionReader)); + oneOf(connectionManager).manageIncomingConnection(with(ID), + with(any(TransportConnectionReader.class)), + with(any(TagController.class))); + will(new DoAllAction( + new CaptureArgumentAction<>(reader, + TransportConnectionReader.class, 1), + new CaptureArgumentAction<>(controller, + TagController.class, 2) + )); + }}); + } +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/plugin/file/RemovableDriveIntegrationTestComponent.java b/bramble-core/src/test/java/org/briarproject/bramble/plugin/file/RemovableDriveIntegrationTestComponent.java index 385e140da..32c0471aa 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/plugin/file/RemovableDriveIntegrationTestComponent.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/plugin/file/RemovableDriveIntegrationTestComponent.java @@ -13,6 +13,7 @@ import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule; import org.briarproject.bramble.system.TimeTravelModule; import org.briarproject.bramble.test.TestDatabaseConfigModule; import org.briarproject.bramble.test.TestFeatureFlagModule; +import org.briarproject.bramble.test.TestMailboxDirectoryModule; import org.briarproject.bramble.test.TestSecureRandomModule; import javax.inject.Singleton; @@ -27,6 +28,7 @@ import dagger.Component; DefaultWakefulIoExecutorModule.class, TestDatabaseConfigModule.class, TestFeatureFlagModule.class, + TestMailboxDirectoryModule.class, RemovableDriveIntegrationTestModule.class, RemovableDriveModule.class, TestSecureRandomModule.class, diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java index b735dab7d..6a2055e7e 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/test/BrambleCoreIntegrationTestModule.java @@ -13,6 +13,7 @@ import dagger.Module; DefaultWakefulIoExecutorModule.class, TestDatabaseConfigModule.class, TestFeatureFlagModule.class, + TestMailboxDirectoryModule.class, TestPluginConfigModule.class, TestSecureRandomModule.class, TimeTravelModule.class diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/TestMailboxDirectoryModule.java b/bramble-core/src/test/java/org/briarproject/bramble/test/TestMailboxDirectoryModule.java new file mode 100644 index 000000000..71c5ff58a --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/test/TestMailboxDirectoryModule.java @@ -0,0 +1,18 @@ +package org.briarproject.bramble.test; + +import org.briarproject.bramble.api.mailbox.MailboxDirectory; + +import java.io.File; + +import dagger.Module; +import dagger.Provides; + +@Module +public class TestMailboxDirectoryModule { + + @Provides + @MailboxDirectory + File provideMailboxDirectory() { + return new File("mailbox"); + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java index e6682a751..55ebd6234 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java @@ -14,6 +14,7 @@ import org.briarproject.bramble.api.crypto.PublicKey; import org.briarproject.bramble.api.db.DatabaseConfig; import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.mailbox.MailboxDirectory; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.plugin.BluetoothConstants; import org.briarproject.bramble.api.plugin.LanTcpConstants; @@ -27,6 +28,7 @@ import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory; import org.briarproject.bramble.api.reporting.DevConfig; import org.briarproject.bramble.plugin.bluetooth.AndroidBluetoothPluginFactory; import org.briarproject.bramble.plugin.file.AndroidRemovableDrivePluginFactory; +import org.briarproject.bramble.plugin.file.MailboxPluginFactory; import org.briarproject.bramble.plugin.tcp.AndroidLanTcpPluginFactory; import org.briarproject.bramble.plugin.tor.AndroidTorPluginFactory; import org.briarproject.bramble.util.AndroidUtils; @@ -63,6 +65,7 @@ import org.briarproject.briar.api.test.TestAvatarCreator; import java.io.File; import java.security.GeneralSecurityException; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; @@ -76,7 +79,6 @@ import dagger.Provides; import static android.content.Context.MODE_PRIVATE; import static android.os.Build.VERSION.SDK_INT; import static java.util.Arrays.asList; -import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_CONTROL_PORT; @@ -156,6 +158,13 @@ public class AppModule { return new AndroidDatabaseConfig(dbDir, keyDir, keyStrengthener); } + @Provides + @Singleton + @MailboxDirectory + File provideMailboxDirectory(Application app) { + return app.getDir("mailbox", MODE_PRIVATE); + } + @Provides @Singleton @TorDirectory @@ -190,7 +199,7 @@ public class AppModule { PluginConfig providePluginConfig(AndroidBluetoothPluginFactory bluetooth, AndroidTorPluginFactory tor, AndroidLanTcpPluginFactory lan, AndroidRemovableDrivePluginFactory drive, - FeatureFlags featureFlags) { + MailboxPluginFactory mailbox, FeatureFlags featureFlags) { @NotNullByDefault PluginConfig pluginConfig = new PluginConfig() { @@ -201,8 +210,10 @@ public class AppModule { @Override public Collection getSimplexFactories() { - if (SDK_INT >= 19) return singletonList(drive); - else return emptyList(); + List simplex = new ArrayList<>(); + if (featureFlags.shouldEnableMailbox()) simplex.add(mailbox); + if (SDK_INT >= 19) simplex.add(drive); + return simplex; } @Override diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt index 85cf884d3..e78774011 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.kt @@ -6,6 +6,7 @@ import dagger.Provides import org.briarproject.bramble.account.AccountModule import org.briarproject.bramble.api.FeatureFlags import org.briarproject.bramble.api.db.DatabaseConfig +import org.briarproject.bramble.api.mailbox.MailboxDirectory import org.briarproject.bramble.api.plugin.PluginConfig import org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_CONTROL_PORT import org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_SOCKS_PORT @@ -71,6 +72,12 @@ internal class HeadlessModule(private val appDir: File) { return HeadlessDatabaseConfig(dbDir, keyDir) } + @Provides + @MailboxDirectory + internal fun provideMailboxDirectory(): File { + return File(appDir, "mailbox") + } + @Provides @TorDirectory internal fun provideTorDirectory(): File { diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/HeadlessTestModule.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/HeadlessTestModule.kt index b2be6db86..3c0da66aa 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/HeadlessTestModule.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/HeadlessTestModule.kt @@ -5,6 +5,7 @@ import dagger.Module import dagger.Provides import org.briarproject.bramble.account.AccountModule import org.briarproject.bramble.api.db.DatabaseConfig +import org.briarproject.bramble.api.mailbox.MailboxDirectory import org.briarproject.bramble.api.plugin.PluginConfig import org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_CONTROL_PORT import org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_SOCKS_PORT @@ -68,6 +69,12 @@ internal class HeadlessTestModule(private val appDir: File) { return HeadlessDatabaseConfig(dbDir, keyDir) } + @Provides + @MailboxDirectory + internal fun provideMailboxDirectory(): File { + return File(appDir, "mailbox") + } + @Provides @TorSocksPort internal fun provideTorSocksPort(): Int = DEFAULT_SOCKS_PORT