diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxManager.java new file mode 100644 index 000000000..dabfb9ea4 --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxManager.java @@ -0,0 +1,35 @@ +package org.briarproject.bramble.api.mailbox; + +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; + +import javax.annotation.Nullable; + +public interface MailboxManager { + + /** + * @return true if a mailbox is already paired. + */ + boolean isPaired(Transaction txn) throws DbException; + + /** + * @return the current status of the mailbox. + */ + MailboxStatus getMailboxStatus(Transaction txn) throws DbException; + + /** + * Returns the currently running pairing task, + * or null if no pairing task is running. + */ + @Nullable + MailboxPairingTask getCurrentPairingTask(); + + /** + * Starts and returns a pairing task. If a pairing task is already running, + * it will be returned and the argument will be ignored. + * + * @param qrCodePayload The ISO-8859-1 encoded bytes of the mailbox QR code. + */ + MailboxPairingTask startPairingTask(String qrCodePayload); + +} diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxPairingState.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxPairingState.java new file mode 100644 index 000000000..8581da441 --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxPairingState.java @@ -0,0 +1,59 @@ +package org.briarproject.bramble.api.mailbox; + +import javax.annotation.Nullable; + +public abstract class MailboxPairingState { + + /** + * The QR code payload that was scanned by the user. + * This is null if the code should not be re-used anymore in this state. + */ + @Nullable + public final String qrCodePayload; + + MailboxPairingState(@Nullable String qrCodePayload) { + this.qrCodePayload = qrCodePayload; + } + + public static class QrCodeReceived extends MailboxPairingState { + public QrCodeReceived(String qrCodePayload) { + super(qrCodePayload); + } + } + + public static class Pairing extends MailboxPairingState { + public Pairing(String qrCodePayload) { + super(qrCodePayload); + } + } + + public static class Paired extends MailboxPairingState { + public Paired() { + super(null); + } + } + + public static class InvalidQrCode extends MailboxPairingState { + public InvalidQrCode() { + super(null); + } + } + + public static class MailboxAlreadyPaired extends MailboxPairingState { + public MailboxAlreadyPaired() { + super(null); + } + } + + public static class ConnectionError extends MailboxPairingState { + public ConnectionError(String qrCodePayload) { + super(qrCodePayload); + } + } + + public static class UnexpectedError extends MailboxPairingState { + public UnexpectedError(String qrCodePayload) { + super(qrCodePayload); + } + } +} diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxPairingTask.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxPairingTask.java new file mode 100644 index 000000000..0f8cd05d7 --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxPairingTask.java @@ -0,0 +1,21 @@ +package org.briarproject.bramble.api.mailbox; + +import org.briarproject.bramble.api.Consumer; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +@NotNullByDefault +public interface MailboxPairingTask extends Runnable { + + /** + * Adds an observer to the task. The observer will be notified on the + * event thread of the current state of the task and any subsequent state + * changes. + */ + void addObserver(Consumer observer); + + /** + * Removes an observer from the task. + */ + void removeObserver(Consumer observer); + +} diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxProperties.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxProperties.java index 26025fc8e..7d8d3c5c4 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxProperties.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxProperties.java @@ -8,19 +8,19 @@ import javax.annotation.concurrent.Immutable; @NotNullByDefault public class MailboxProperties { - private final String onionAddress; + private final String baseUrl; private final MailboxAuthToken authToken; private final boolean owner; - public MailboxProperties(String onionAddress, MailboxAuthToken authToken, + public MailboxProperties(String baseUrl, MailboxAuthToken authToken, boolean owner) { - this.onionAddress = onionAddress; + this.baseUrl = baseUrl; this.authToken = authToken; this.owner = owner; } - public String getOnionAddress() { - return onionAddress; + public String getBaseUrl() { + return baseUrl; } public MailboxAuthToken getAuthToken() { diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApi.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApi.java index 6b9bf2203..63c494f03 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApi.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApi.java @@ -163,6 +163,10 @@ interface MailboxApi { class ApiException extends Exception { } + @Immutable + class MailboxAlreadyPairedException extends ApiException { + } + /** * A failure that does not need to be retried, * e.g. when adding a contact that already exists. diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApiImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApiImpl.java index ee20aac3a..7d18b7aa0 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApiImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxApiImpl.java @@ -60,13 +60,12 @@ class MailboxApiImpl implements MailboxApi { throws IOException, ApiException { if (!properties.isOwner()) throw new IllegalArgumentException(); Request request = getRequestBuilder(properties.getAuthToken()) - .url(properties.getOnionAddress() + "/setup") + .url(properties.getBaseUrl() + "/setup") .put(EMPTY_REQUEST) .build(); OkHttpClient client = httpClientProvider.get(); Response response = client.newCall(request).execute(); - // TODO consider throwing a special exception for the 401 case - if (response.code() == 401) throw new ApiException(); + if (response.code() == 401) throw new MailboxAlreadyPairedException(); if (!response.isSuccessful()) throw new ApiException(); ResponseBody body = response.body(); if (body == null) throw new ApiException(); @@ -122,7 +121,7 @@ class MailboxApiImpl implements MailboxApi { public void deleteContact(MailboxProperties properties, ContactId contactId) throws IOException, ApiException, TolerableFailureException { if (!properties.isOwner()) throw new IllegalArgumentException(); - String url = properties.getOnionAddress() + "/contacts/" + + String url = properties.getBaseUrl() + "/contacts/" + contactId.getInt(); Request request = getRequestBuilder(properties.getAuthToken()) .delete() @@ -226,7 +225,7 @@ class MailboxApiImpl implements MailboxApi { String path = "/files/" + folderId + "/" + fileId; Request request = getRequestBuilder(properties.getAuthToken()) .delete() - .url(properties.getOnionAddress() + path) + .url(properties.getBaseUrl() + path) .build(); OkHttpClient client = httpClientProvider.get(); Response response = client.newCall(request).execute(); @@ -268,7 +267,7 @@ class MailboxApiImpl implements MailboxApi { private Response sendGetRequest(MailboxProperties properties, String path) throws IOException { Request request = getRequestBuilder(properties.getAuthToken()) - .url(properties.getOnionAddress() + path) + .url(properties.getBaseUrl() + path) .build(); OkHttpClient client = httpClientProvider.get(); return client.newCall(request).execute(); @@ -277,7 +276,7 @@ class MailboxApiImpl implements MailboxApi { private Response sendPostRequest(MailboxProperties properties, String path, RequestBody body) throws IOException { Request request = getRequestBuilder(properties.getAuthToken()) - .url(properties.getOnionAddress() + path) + .url(properties.getBaseUrl() + path) .post(body) .build(); OkHttpClient client = httpClientProvider.get(); diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxManagerImpl.java new file mode 100644 index 000000000..7bbeb0e12 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxManagerImpl.java @@ -0,0 +1,78 @@ +package org.briarproject.bramble.mailbox; + +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.lifecycle.IoExecutor; +import org.briarproject.bramble.api.mailbox.MailboxManager; +import org.briarproject.bramble.api.mailbox.MailboxPairingTask; +import org.briarproject.bramble.api.mailbox.MailboxSettingsManager; +import org.briarproject.bramble.api.mailbox.MailboxStatus; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import java.util.concurrent.Executor; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +@Immutable +@NotNullByDefault +class MailboxManagerImpl implements MailboxManager { + + private final Executor ioExecutor; + private final MailboxSettingsManager mailboxSettingsManager; + private final MailboxPairingTaskFactory pairingTaskFactory; + private final Object lock = new Object(); + + @Nullable + @GuardedBy("lock") + private MailboxPairingTask pairingTask = null; + + @Inject + MailboxManagerImpl( + @IoExecutor Executor ioExecutor, + MailboxSettingsManager mailboxSettingsManager, + MailboxPairingTaskFactory pairingTaskFactory) { + this.ioExecutor = ioExecutor; + this.mailboxSettingsManager = mailboxSettingsManager; + this.pairingTaskFactory = pairingTaskFactory; + } + + @Override + public boolean isPaired(Transaction txn) throws DbException { + return mailboxSettingsManager.getOwnMailboxProperties(txn) != null; + } + + @Override + public MailboxStatus getMailboxStatus(Transaction txn) throws DbException { + return mailboxSettingsManager.getOwnMailboxStatus(txn); + } + + @Nullable + @Override + public MailboxPairingTask getCurrentPairingTask() { + synchronized (lock) { + return pairingTask; + } + } + + @Override + public MailboxPairingTask startPairingTask(String payload) { + MailboxPairingTask created; + synchronized (lock) { + if (pairingTask != null) return pairingTask; + created = pairingTaskFactory.createPairingTask(payload); + pairingTask = created; + } + ioExecutor.execute(() -> { + created.run(); + synchronized (lock) { + // remove task after it finished + pairingTask = null; + } + }); + return created; + } + +} 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 a01b15098..85e925202 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 @@ -1,16 +1,36 @@ package org.briarproject.bramble.mailbox; +import org.briarproject.bramble.api.mailbox.MailboxManager; import org.briarproject.bramble.api.mailbox.MailboxSettingsManager; +import javax.inject.Singleton; + import dagger.Module; import dagger.Provides; @Module public class MailboxModule { + @Provides + @Singleton + MailboxManager providesMailboxManager(MailboxManagerImpl mailboxManager) { + return mailboxManager; + } + + @Provides + MailboxPairingTaskFactory provideMailboxPairingTaskFactory( + MailboxPairingTaskFactoryImpl mailboxPairingTaskFactory) { + return mailboxPairingTaskFactory; + } + @Provides MailboxSettingsManager provideMailboxSettingsManager( MailboxSettingsManagerImpl mailboxSettingsManager) { return mailboxSettingsManager; } + + @Provides + MailboxApi providesMailboxApi(MailboxApiImpl mailboxApi) { + return mailboxApi; + } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskFactory.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskFactory.java new file mode 100644 index 000000000..749a7f31c --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskFactory.java @@ -0,0 +1,12 @@ +package org.briarproject.bramble.mailbox; + + +import org.briarproject.bramble.api.mailbox.MailboxPairingTask; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +@NotNullByDefault +interface MailboxPairingTaskFactory { + + MailboxPairingTask createPairingTask(String qrCodePayload); + +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskFactoryImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskFactoryImpl.java new file mode 100644 index 000000000..6c5150eb3 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskFactoryImpl.java @@ -0,0 +1,48 @@ +package org.briarproject.bramble.mailbox; + +import org.briarproject.bramble.api.crypto.CryptoComponent; +import org.briarproject.bramble.api.db.TransactionManager; +import org.briarproject.bramble.api.event.EventExecutor; +import org.briarproject.bramble.api.mailbox.MailboxPairingTask; +import org.briarproject.bramble.api.mailbox.MailboxSettingsManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.system.Clock; + +import java.util.concurrent.Executor; + +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +@Immutable +@NotNullByDefault +class MailboxPairingTaskFactoryImpl implements MailboxPairingTaskFactory { + + private final Executor eventExecutor; + private final TransactionManager db; + private final CryptoComponent crypto; + private final Clock clock; + private final MailboxApi api; + private final MailboxSettingsManager mailboxSettingsManager; + + @Inject + MailboxPairingTaskFactoryImpl( + @EventExecutor Executor eventExecutor, + TransactionManager db, + CryptoComponent crypto, + Clock clock, + MailboxApi api, + MailboxSettingsManager mailboxSettingsManager) { + this.eventExecutor = eventExecutor; + this.db = db; + this.crypto = crypto; + this.clock = clock; + this.api = api; + this.mailboxSettingsManager = mailboxSettingsManager; + } + + @Override + public MailboxPairingTask createPairingTask(String qrCodePayload) { + return new MailboxPairingTaskImpl(qrCodePayload, eventExecutor, db, + crypto, clock, api, mailboxSettingsManager); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskImpl.java new file mode 100644 index 000000000..03f36d2f7 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxPairingTaskImpl.java @@ -0,0 +1,172 @@ +package org.briarproject.bramble.mailbox; + +import org.briarproject.bramble.api.Consumer; +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.crypto.CryptoComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.TransactionManager; +import org.briarproject.bramble.api.event.EventExecutor; +import org.briarproject.bramble.api.mailbox.MailboxAuthToken; +import org.briarproject.bramble.api.mailbox.MailboxPairingState; +import org.briarproject.bramble.api.mailbox.MailboxPairingTask; +import org.briarproject.bramble.api.mailbox.MailboxProperties; +import org.briarproject.bramble.api.mailbox.MailboxSettingsManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.mailbox.MailboxApi.ApiException; +import org.briarproject.bramble.mailbox.MailboxApi.MailboxAlreadyPairedException; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.LogUtils.logException; + +@ThreadSafe +@NotNullByDefault +class MailboxPairingTaskImpl implements MailboxPairingTask { + + private final static Logger LOG = + getLogger(MailboxPairingTaskImpl.class.getName()); + @SuppressWarnings("CharsetObjectCanBeUsed") // Requires minSdkVersion >= 19 + private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); + private static final int VERSION_REQUIRED = 32; + + private final String payload; + private final Executor eventExecutor; + private final TransactionManager db; + private final CryptoComponent crypto; + private final Clock clock; + private final MailboxApi api; + private final MailboxSettingsManager mailboxSettingsManager; + + private final Object lock = new Object(); + @GuardedBy("lock") + private final List> observers = + new ArrayList<>(); + @GuardedBy("lock") + private MailboxPairingState state; + + MailboxPairingTaskImpl( + String payload, + @EventExecutor Executor eventExecutor, + TransactionManager db, + CryptoComponent crypto, + Clock clock, + MailboxApi api, + MailboxSettingsManager mailboxSettingsManager) { + this.payload = payload; + this.eventExecutor = eventExecutor; + this.db = db; + this.crypto = crypto; + this.clock = clock; + this.api = api; + this.mailboxSettingsManager = mailboxSettingsManager; + state = new MailboxPairingState.QrCodeReceived(payload); + } + + @Override + public void addObserver(Consumer o) { + MailboxPairingState state; + synchronized (lock) { + observers.add(o); + state = this.state; + eventExecutor.execute(() -> o.accept(state)); + } + } + + @Override + public void removeObserver(Consumer o) { + synchronized (lock) { + observers.remove(o); + } + } + + @Override + public void run() { + try { + pairMailbox(); + } catch (FormatException e) { + onMailboxError(e, new MailboxPairingState.InvalidQrCode()); + } catch (MailboxAlreadyPairedException e) { + onMailboxError(e, new MailboxPairingState.MailboxAlreadyPaired()); + } catch (IOException e) { + onMailboxError(e, new MailboxPairingState.ConnectionError(payload)); + } catch (ApiException | DbException e) { + onMailboxError(e, new MailboxPairingState.UnexpectedError(payload)); + } + } + + private void pairMailbox() throws IOException, ApiException, DbException { + MailboxProperties mailboxProperties = decodeQrCodePayload(payload); + setState(new MailboxPairingState.Pairing(payload)); + MailboxAuthToken ownerToken = api.setup(mailboxProperties); + MailboxProperties ownerProperties = new MailboxProperties( + mailboxProperties.getBaseUrl(), ownerToken, true); + long time = clock.currentTimeMillis(); + db.transaction(false, txn -> { + mailboxSettingsManager + .setOwnMailboxProperties(txn, ownerProperties); + mailboxSettingsManager.recordSuccessfulConnection(txn, time); + }); + setState(new MailboxPairingState.Paired()); + } + + private void onMailboxError(Exception e, MailboxPairingState state) { + logException(LOG, WARNING, e); + setState(state); + } + + private void setState(MailboxPairingState state) { + synchronized (lock) { + this.state = state; + notifyObservers(); + } + } + + @GuardedBy("lock") + private void notifyObservers() { + List> observers = + new ArrayList<>(this.observers); + MailboxPairingState state = this.state; + eventExecutor.execute(() -> { + for (Consumer o : observers) o.accept(state); + }); + } + + private MailboxProperties decodeQrCodePayload(String payload) + throws FormatException { + byte[] bytes = payload.getBytes(ISO_8859_1); + if (bytes.length != 65) { + if (LOG.isLoggable(WARNING)) { + LOG.warning("QR code length is not 65: " + bytes.length); + } + throw new FormatException(); + } + int version = bytes[0] & 0xFF; + if (version != VERSION_REQUIRED) { + if (LOG.isLoggable(WARNING)) { + LOG.warning("QR code has not version " + VERSION_REQUIRED + + ": " + version); + } + throw new FormatException(); + } + LOG.info("QR code is valid"); + byte[] onionPubKey = Arrays.copyOfRange(bytes, 1, 33); + String onionAddress = crypto.encodeOnionAddress(onionPubKey); + String baseUrl = "http://" + onionAddress + ".onion"; + byte[] tokenBytes = Arrays.copyOfRange(bytes, 33, 65); + MailboxAuthToken setupToken = new MailboxAuthToken(tokenBytes); + return new MailboxProperties(baseUrl, setupToken, true); + } + +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxSettingsManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxSettingsManagerImpl.java index bfb9f085e..4e0065037 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxSettingsManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/MailboxSettingsManagerImpl.java @@ -57,7 +57,7 @@ class MailboxSettingsManagerImpl implements MailboxSettingsManager { public void setOwnMailboxProperties(Transaction txn, MailboxProperties p) throws DbException { Settings s = new Settings(); - s.put(SETTINGS_KEY_ONION, p.getOnionAddress()); + s.put(SETTINGS_KEY_ONION, p.getBaseUrl()); s.put(SETTINGS_KEY_TOKEN, p.getAuthToken().toString()); settingsManager.mergeSettings(txn, s, SETTINGS_NAMESPACE); } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveTaskRegistry.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveTaskRegistry.java index 1f073bbf2..ca29b473e 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveTaskRegistry.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/RemovableDriveTaskRegistry.java @@ -3,6 +3,7 @@ package org.briarproject.bramble.plugin.file; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.plugin.file.RemovableDriveTask; +@Deprecated // We can simply remove tasks when they finish @NotNullByDefault interface RemovableDriveTaskRegistry { diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxIntegrationTest.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxIntegrationTest.java index 31554b20e..d92d4ae77 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxIntegrationTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxIntegrationTest.java @@ -151,7 +151,7 @@ public class MailboxIntegrationTest extends BrambleTestCase { ContactId contactId = new ContactId(1); MailboxContact contact = getMailboxContact(contactId); MailboxProperties contactProperties = new MailboxProperties( - ownerProperties.getOnionAddress(), contact.token, false); + ownerProperties.getBaseUrl(), contact.token, false); api.addContact(ownerProperties, contact); // upload a file for our contact diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxPairingTaskImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxPairingTaskImplTest.java new file mode 100644 index 000000000..7b8d70779 --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxPairingTaskImplTest.java @@ -0,0 +1,196 @@ +package org.briarproject.bramble.mailbox; + +import org.briarproject.bramble.api.crypto.CryptoComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.db.TransactionManager; +import org.briarproject.bramble.api.mailbox.MailboxAuthToken; +import org.briarproject.bramble.api.mailbox.MailboxPairingState; +import org.briarproject.bramble.api.mailbox.MailboxPairingTask; +import org.briarproject.bramble.api.mailbox.MailboxProperties; +import org.briarproject.bramble.api.mailbox.MailboxSettingsManager; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.test.BrambleMockTestCase; +import org.briarproject.bramble.test.DbExpectations; +import org.briarproject.bramble.test.ImmediateExecutor; +import org.briarproject.bramble.test.PredicateMatcher; +import org.jmock.Expectations; +import org.junit.Test; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.briarproject.bramble.test.TestUtils.getRandomBytes; +import static org.briarproject.bramble.test.TestUtils.getRandomId; +import static org.briarproject.bramble.util.StringUtils.getRandomString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class MailboxPairingTaskImplTest extends BrambleMockTestCase { + + private final Executor executor = new ImmediateExecutor(); + private final TransactionManager db = + context.mock(TransactionManager.class); + private final CryptoComponent crypto = context.mock(CryptoComponent.class); + private final Clock clock = context.mock(Clock.class); + private final MailboxApi api = context.mock(MailboxApi.class); + private final MailboxSettingsManager mailboxSettingsManager = + context.mock(MailboxSettingsManager.class); + private final MailboxPairingTaskFactory factory = + new MailboxPairingTaskFactoryImpl(executor, db, crypto, clock, api, + mailboxSettingsManager); + + private final String onion = getRandomString(56); + private final byte[] onionBytes = getRandomBytes(32); + private final String onionAddress = "http://" + onion + ".onion"; + private final MailboxAuthToken setupToken = + new MailboxAuthToken(getRandomId()); + private final MailboxAuthToken ownerToken = + new MailboxAuthToken(getRandomId()); + private final String validPayload = getValidPayload(); + private final long time = System.currentTimeMillis(); + private final MailboxProperties setupProperties = + new MailboxProperties(onionAddress, setupToken, true); + private final MailboxProperties ownerProperties = + new MailboxProperties(onionAddress, ownerToken, true); + + @Test + public void testInitialQrCodeReceivedState() { + MailboxPairingTask task = + factory.createPairingTask(getRandomString(42)); + task.addObserver(state -> + assertTrue(state instanceof MailboxPairingState.QrCodeReceived) + ); + } + + @Test + public void testInvalidQrCode() { + MailboxPairingTask task1 = + factory.createPairingTask(getRandomString(42)); + task1.run(); + task1.addObserver(state -> + assertTrue(state instanceof MailboxPairingState.InvalidQrCode) + ); + + String goodLength = "00" + getRandomString(63); + MailboxPairingTask task2 = factory.createPairingTask(goodLength); + task2.run(); + task2.addObserver(state -> + assertTrue(state instanceof MailboxPairingState.InvalidQrCode) + ); + } + + @Test + public void testSuccessfulPairing() throws Exception { + context.checking(new Expectations() {{ + oneOf(crypto).encodeOnionAddress(onionBytes); + will(returnValue(onion)); + oneOf(api).setup(with(matches(setupProperties))); + will(returnValue(ownerToken)); + oneOf(clock).currentTimeMillis(); + will(returnValue(time)); + }}); + Transaction txn = new Transaction(null, false); + context.checking(new DbExpectations() {{ + oneOf(db).transaction(with(false), withDbRunnable(txn)); + oneOf(mailboxSettingsManager).setOwnMailboxProperties( + with(txn), with(matches(ownerProperties))); + oneOf(mailboxSettingsManager).recordSuccessfulConnection(txn, time); + }}); + + AtomicInteger i = new AtomicInteger(0); + MailboxPairingTask task = factory.createPairingTask(validPayload); + task.addObserver(state -> { + if (i.get() == 0) { + assertEquals(MailboxPairingState.QrCodeReceived.class, + state.getClass()); + } else if (i.get() == 1) { + assertEquals(MailboxPairingState.Pairing.class, + state.getClass()); + } else if (i.get() == 2) { + assertEquals(MailboxPairingState.Paired.class, + state.getClass()); + } else fail("Unexpected change of state " + state.getClass()); + i.getAndIncrement(); + }); + task.run(); + } + + @Test + public void testAlreadyPaired() throws Exception { + testApiException(new MailboxApi.MailboxAlreadyPairedException(), + MailboxPairingState.MailboxAlreadyPaired.class); + } + + @Test + public void testMailboxApiException() throws Exception { + testApiException(new MailboxApi.ApiException(), + MailboxPairingState.UnexpectedError.class); + } + + @Test + public void testApiIOException() throws Exception { + testApiException(new IOException(), + MailboxPairingState.ConnectionError.class); + } + + private void testApiException(Exception e, + Class s) throws Exception { + context.checking(new Expectations() {{ + oneOf(crypto).encodeOnionAddress(onionBytes); + will(returnValue(onion)); + oneOf(api).setup(with(matches(setupProperties))); + will(throwException(e)); + }}); + + MailboxPairingTask task = factory.createPairingTask(validPayload); + task.run(); + task.addObserver(state -> assertEquals(state.getClass(), s)); + } + + @Test + public void testDbException() throws Exception { + context.checking(new Expectations() {{ + oneOf(crypto).encodeOnionAddress(onionBytes); + will(returnValue(onion)); + oneOf(api).setup(with(matches(setupProperties))); + will(returnValue(ownerToken)); + oneOf(clock).currentTimeMillis(); + will(returnValue(time)); + }}); + Transaction txn = new Transaction(null, false); + context.checking(new DbExpectations() {{ + oneOf(db).transaction(with(false), withDbRunnable(txn)); + oneOf(mailboxSettingsManager).setOwnMailboxProperties( + with(txn), with(matches(ownerProperties))); + will(throwException(new DbException())); + }}); + + MailboxPairingTask task = factory.createPairingTask(validPayload); + task.run(); + task.addObserver(state -> assertEquals(state.getClass(), + MailboxPairingState.UnexpectedError.class)); + } + + private String getValidPayload() { + byte[] payloadBytes = ByteBuffer.allocate(65) + .put((byte) 32) // 1 + .put(onionBytes) // 32 + .put(setupToken.getBytes()) // 32 + .array(); + //noinspection CharsetObjectCanBeUsed + return new String(payloadBytes, Charset.forName("ISO-8859-1")); + } + + private PredicateMatcher matches(MailboxProperties p2) { + return new PredicateMatcher<>(MailboxProperties.class, p1 -> + p1.getAuthToken().equals(p2.getAuthToken()) && + p1.getBaseUrl().equals(p2.getBaseUrl()) && + p1.isOwner() == p2.isOwner()); + } + +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxSettingsManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxSettingsManagerImplTest.java index 57aa8894a..21fe9266f 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxSettingsManagerImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxSettingsManagerImplTest.java @@ -73,7 +73,7 @@ public class MailboxSettingsManagerImplTest extends BrambleMockTestCase { MailboxProperties properties = manager.getOwnMailboxProperties(txn); assertNotNull(properties); - assertEquals(onion, properties.getOnionAddress()); + assertEquals(onion, properties.getBaseUrl()); assertEquals(token, properties.getAuthToken()); assertTrue(properties.isOwner()); }