diff --git a/bramble-api/build.gradle b/bramble-api/build.gradle index 0cd52e31a..926386b02 100644 --- a/bramble-api/build.gradle +++ b/bramble-api/build.gradle @@ -9,6 +9,7 @@ apply from: 'witness.gradle' dependencies { implementation "com.google.dagger:dagger:$dagger_version" implementation 'com.google.code.findbugs:jsr305:3.0.2' + implementation "com.fasterxml.jackson.core:jackson-annotations:$jackson_version" testImplementation "junit:junit:$junit_version" testImplementation "org.jmock:jmock:$jmock_version" diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/InvalidMailboxIdException.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/InvalidMailboxIdException.java new file mode 100644 index 000000000..dfb54d84e --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/InvalidMailboxIdException.java @@ -0,0 +1,8 @@ +package org.briarproject.bramble.api.mailbox; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +@NotNullByDefault +public class InvalidMailboxIdException extends Exception { + +} diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxAuthToken.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxAuthToken.java new file mode 100644 index 000000000..d2b932969 --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxAuthToken.java @@ -0,0 +1,24 @@ +package org.briarproject.bramble.api.mailbox; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +@ThreadSafe +@NotNullByDefault +public class MailboxAuthToken extends MailboxId { + public MailboxAuthToken(byte[] id) { + super(id); + } + + /** + * Creates a {@link MailboxAuthToken} from the given string. + * + * @throws InvalidMailboxIdException if token is not valid. + */ + public static MailboxAuthToken fromString(@Nullable String token) + throws InvalidMailboxIdException { + return new MailboxAuthToken(bytesFromString(token)); + } +} diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxFileId.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxFileId.java new file mode 100644 index 000000000..7814658da --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxFileId.java @@ -0,0 +1,24 @@ +package org.briarproject.bramble.api.mailbox; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +@ThreadSafe +@NotNullByDefault +public class MailboxFileId extends MailboxId { + public MailboxFileId(byte[] id) { + super(id); + } + + /** + * Creates a {@link MailboxFileId} from the given string. + * + * @throws IllegalArgumentException if token is not valid. + */ + public static MailboxFileId fromString(@Nullable String token) + throws InvalidMailboxIdException { + return new MailboxFileId(bytesFromString(token)); + } +} diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxFolderId.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxFolderId.java new file mode 100644 index 000000000..7a1819d04 --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxFolderId.java @@ -0,0 +1,24 @@ +package org.briarproject.bramble.api.mailbox; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +@ThreadSafe +@NotNullByDefault +public class MailboxFolderId extends MailboxId { + public MailboxFolderId(byte[] id) { + super(id); + } + + /** + * Creates a {@link MailboxFolderId} from the given string. + * + * @throws IllegalArgumentException if token is not valid. + */ + public static MailboxFolderId fromString(@Nullable String token) + throws InvalidMailboxIdException { + return new MailboxFolderId(bytesFromString(token)); + } +} diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxId.java b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxId.java new file mode 100644 index 000000000..06719fb80 --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/mailbox/MailboxId.java @@ -0,0 +1,49 @@ +package org.briarproject.bramble.api.mailbox; + +import com.fasterxml.jackson.annotation.JsonValue; + +import org.briarproject.bramble.api.UniqueId; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import java.util.Locale; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +import static org.briarproject.bramble.util.StringUtils.fromHexString; +import static org.briarproject.bramble.util.StringUtils.toHexString; + +@ThreadSafe +@NotNullByDefault +public abstract class MailboxId extends UniqueId { + MailboxId(byte[] id) { + super(id); + } + + /** + * Returns valid {@link MailboxId} bytes from the given string. + * + * @throws InvalidMailboxIdException if token is not valid. + */ + static byte[] bytesFromString(@Nullable String token) + throws InvalidMailboxIdException { + if (token == null || token.length() != 64) { + throw new InvalidMailboxIdException(); + } + try { + return fromHexString(token); + } catch (IllegalArgumentException e) { + throw new InvalidMailboxIdException(); + } + } + + /** + * Returns the string representation expected by the mailbox API. + * Also used for serialization. + */ + @Override + @JsonValue + public String toString() { + return toHexString(getBytes()).toLowerCase(Locale.US); + } +} 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 723c5aabc..26025fc8e 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,10 +8,11 @@ import javax.annotation.concurrent.Immutable; @NotNullByDefault public class MailboxProperties { - private final String onionAddress, authToken; + private final String onionAddress; + private final MailboxAuthToken authToken; private final boolean owner; - public MailboxProperties(String onionAddress, String authToken, + public MailboxProperties(String onionAddress, MailboxAuthToken authToken, boolean owner) { this.onionAddress = onionAddress; this.authToken = authToken; @@ -22,7 +23,7 @@ public class MailboxProperties { return onionAddress; } - public String getAuthToken() { + public MailboxAuthToken getAuthToken() { return authToken; } diff --git a/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java b/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java index 3fd19912a..f5d1b9ed1 100644 --- a/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java +++ b/bramble-api/src/test/java/org/briarproject/bramble/test/TestUtils.java @@ -25,13 +25,16 @@ import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.util.IoUtils; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; @@ -46,8 +49,8 @@ import static org.briarproject.bramble.api.properties.TransportPropertyConstants import static org.briarproject.bramble.api.sync.ClientId.MAX_CLIENT_ID_LENGTH; import static org.briarproject.bramble.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH; import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH; +import static org.briarproject.bramble.util.IoUtils.copyAndClose; import static org.briarproject.bramble.util.StringUtils.getRandomString; -import static org.briarproject.bramble.util.StringUtils.toHexString; public class TestUtils { @@ -211,8 +214,22 @@ public class TestUtils { getAgreementPublicKey(), verified); } - public static String getMailboxSecret() { - return toHexString(getRandomBytes(32)).toLowerCase(Locale.US); + public static void writeBytes(File file, byte[] bytes) + throws IOException { + FileOutputStream outputStream = new FileOutputStream(file); + //noinspection TryFinallyCanBeTryWithResources + try { + outputStream.write(bytes); + } finally { + outputStream.close(); + } + } + + public static byte[] readBytes(File file) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + FileInputStream inputStream = new FileInputStream(file); + copyAndClose(inputStream, outputStream); + return outputStream.toByteArray(); } public static double getMedian(Collection samples) { 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 be08cc11a..82a628766 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 @@ -3,11 +3,17 @@ package org.briarproject.bramble.mailbox; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.mailbox.MailboxAuthToken; +import org.briarproject.bramble.api.mailbox.MailboxFileId; +import org.briarproject.bramble.api.mailbox.MailboxFolderId; import org.briarproject.bramble.api.mailbox.MailboxProperties; +import java.io.File; import java.io.IOException; import java.util.Collection; +import java.util.List; +import javax.annotation.Nonnull; import javax.annotation.concurrent.Immutable; interface MailboxApi { @@ -19,7 +25,7 @@ interface MailboxApi { * @return the owner token * @throws ApiException for 401 response. */ - String setup(MailboxProperties properties) + MailboxAuthToken setup(MailboxProperties properties) throws IOException, ApiException; /** @@ -57,16 +63,69 @@ interface MailboxApi { Collection getContacts(MailboxProperties properties) throws IOException, ApiException; + /** + * Used by contacts to send files to the owner + * and by the owner to send files to contacts. + *

+ * The owner can add files to the contacts' inboxes + * and the contacts can add files to their own outbox. + */ + void addFile(MailboxProperties properties, MailboxFolderId folderId, + File file) throws IOException, ApiException; + + /** + * Used by owner and contacts to list their files to retrieve. + *

+ * Returns 200 OK with the list of files in JSON. + */ + List getFiles(MailboxProperties properties, + MailboxFolderId folderId) throws IOException, ApiException; + + /** + * Used by owner and contacts to retrieve a file. + *

+ * Returns 200 OK if successful with the files' raw bytes + * in the response body. + * + * @param file the empty file the response bytes will be written into. + */ + void getFile(MailboxProperties properties, MailboxFolderId folderId, + MailboxFileId fileId, File file) throws IOException, ApiException; + + /** + * Used by owner and contacts to delete files. + *

+ * Returns 200 OK (no exception) if deletion was successful. + * + * @throws TolerableFailureException on 404 response, + * because file was most likely deleted already. + */ + void deleteFile(MailboxProperties properties, MailboxFolderId folderId, + MailboxFileId fileId) + throws IOException, ApiException, TolerableFailureException; + + /** + * Lists all contact outboxes that have files available + * for the owner to download. + * + * @return a list of folder names + * to be used with {@link #getFiles(MailboxProperties, MailboxFolderId)}. + * @throws IllegalArgumentException if used by non-owner. + */ + List getFolders(MailboxProperties properties) + throws IOException, ApiException; + @Immutable @JsonSerialize class MailboxContact { public final int contactId; - public final String token, inboxId, outboxId; + public final MailboxAuthToken token; + public final MailboxFolderId inboxId, outboxId; MailboxContact(ContactId contactId, - String token, - String inboxId, - String outboxId) { + MailboxAuthToken token, + MailboxFolderId inboxId, + MailboxFolderId outboxId) { this.contactId = contactId.getInt(); this.token = token; this.inboxId = inboxId; @@ -74,6 +133,24 @@ interface MailboxApi { } } + @JsonSerialize + class MailboxFile implements Comparable { + public final MailboxFileId name; + public final long time; + + public MailboxFile(MailboxFileId name, long time) { + this.name = name; + this.time = time; + } + + @Override + public int compareTo(@Nonnull MailboxApi.MailboxFile mailboxFile) { + //noinspection UseCompareMethod + return time < mailboxFile.time ? -1 : + (time == mailboxFile.time ? 0 : 1); + } + } + @Immutable class ApiException extends Exception { } 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 ba24a852d..9c80bdda7 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 @@ -3,15 +3,25 @@ package org.briarproject.bramble.mailbox; import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.briarproject.bramble.api.WeakSingletonProvider; import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.mailbox.InvalidMailboxIdException; +import org.briarproject.bramble.api.mailbox.MailboxAuthToken; +import org.briarproject.bramble.api.mailbox.MailboxFileId; +import org.briarproject.bramble.api.mailbox.MailboxFolderId; +import org.briarproject.bramble.api.mailbox.MailboxId; import org.briarproject.bramble.api.mailbox.MailboxProperties; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import javax.inject.Inject; @@ -26,7 +36,7 @@ import okhttp3.ResponseBody; import static com.fasterxml.jackson.databind.MapperFeature.BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES; import static java.util.Objects.requireNonNull; import static okhttp3.internal.Util.EMPTY_REQUEST; -import static org.briarproject.bramble.util.StringUtils.fromHexString; +import static org.briarproject.bramble.util.IoUtils.copyAndClose; @NotNullByDefault class MailboxApiImpl implements MailboxApi { @@ -37,6 +47,8 @@ class MailboxApiImpl implements MailboxApi { .build(); private static final MediaType JSON = requireNonNull(MediaType.parse("application/json; charset=utf-8")); + private static final MediaType FILE = + requireNonNull(MediaType.parse("application/octet-stream")); @Inject MailboxApiImpl(WeakSingletonProvider httpClientProvider) { @@ -44,7 +56,7 @@ class MailboxApiImpl implements MailboxApi { } @Override - public String setup(MailboxProperties properties) + public MailboxAuthToken setup(MailboxProperties properties) throws IOException, ApiException { if (!properties.isOwner()) throw new IllegalArgumentException(); Request request = getRequestBuilder(properties.getAuthToken()) @@ -65,26 +77,12 @@ class MailboxApiImpl implements MailboxApi { throw new ApiException(); } String ownerToken = tokenNode.textValue(); - if (ownerToken == null || !isValidToken(ownerToken)) { - throw new ApiException(); - } - return ownerToken; - } catch (JacksonException e) { + return MailboxAuthToken.fromString(ownerToken); + } catch (JacksonException | InvalidMailboxIdException e) { throw new ApiException(); } } - private boolean isValidToken(String token) { - if (token.length() != 64) return false; - try { - // try to convert to bytes - fromHexString(token); - return true; - } catch (IllegalArgumentException e) { - return false; - } - } - @Override public boolean checkStatus(MailboxProperties properties) throws IOException, ApiException { @@ -94,19 +92,15 @@ class MailboxApiImpl implements MailboxApi { return response.isSuccessful(); } + /* Contact Management API (owner only) */ + @Override public void addContact(MailboxProperties properties, MailboxContact contact) - throws IOException, ApiException, - TolerableFailureException { + throws IOException, ApiException, TolerableFailureException { if (!properties.isOwner()) throw new IllegalArgumentException(); byte[] bodyBytes = mapper.writeValueAsBytes(contact); RequestBody body = RequestBody.create(JSON, bodyBytes); - Request request = getRequestBuilder(properties.getAuthToken()) - .url(properties.getOnionAddress() + "/contacts") - .post(body) - .build(); - OkHttpClient client = httpClientProvider.get(); - Response response = client.newCall(request).execute(); + Response response = sendPostRequest(properties, "/contacts", body); if (response.code() == 409) throw new TolerableFailureException(); if (!response.isSuccessful()) throw new ApiException(); } @@ -138,10 +132,7 @@ class MailboxApiImpl implements MailboxApi { if (body == null) throw new ApiException(); try { JsonNode node = mapper.readTree(body.string()); - JsonNode contactsNode = node.get("contacts"); - if (contactsNode == null || !contactsNode.isArray()) { - throw new ApiException(); - } + ArrayNode contactsNode = getArray(node, "contacts"); List list = new ArrayList<>(); for (JsonNode contactNode : contactsNode) { if (!contactNode.isNumber()) throw new ApiException(); @@ -155,6 +146,112 @@ class MailboxApiImpl implements MailboxApi { } } + /* File Management (owner and contacts) */ + + @Override + public void addFile(MailboxProperties properties, MailboxFolderId folderId, + File file) throws IOException, ApiException { + String path = "/files/" + folderId; + RequestBody body = RequestBody.create(FILE, file); + Response response = sendPostRequest(properties, path, body); + if (response.code() != 200) throw new ApiException(); + } + + @Override + public List getFiles(MailboxProperties properties, + MailboxFolderId folderId) throws IOException, ApiException { + String path = "/files/" + folderId; + Response response = sendGetRequest(properties, path); + if (response.code() != 200) throw new ApiException(); + + ResponseBody body = response.body(); + if (body == null) throw new ApiException(); + try { + JsonNode node = mapper.readTree(body.string()); + ArrayNode filesNode = getArray(node, "files"); + List list = new ArrayList<>(); + for (JsonNode fileNode : filesNode) { + if (!fileNode.isObject()) throw new ApiException(); + ObjectNode objectNode = (ObjectNode) fileNode; + JsonNode nameNode = objectNode.get("name"); + JsonNode timeNode = objectNode.get("time"); + if (nameNode == null || !nameNode.isTextual()) { + throw new ApiException(); + } + if (timeNode == null || !timeNode.isNumber()) { + throw new ApiException(); + } + String name = nameNode.asText(); + long time = timeNode.asLong(); + if (time < 1) throw new ApiException(); + list.add(new MailboxFile(MailboxFileId.fromString(name), time)); + } + Collections.sort(list); + return list; + } catch (JacksonException | InvalidMailboxIdException e) { + throw new ApiException(); + } + } + + @Override + public void getFile(MailboxProperties properties, MailboxFolderId folderId, + MailboxFileId fileId, File file) throws IOException, ApiException { + String path = "/files/" + folderId + "/" + fileId; + Response response = sendGetRequest(properties, path); + if (response.code() != 200) throw new ApiException(); + + ResponseBody body = response.body(); + if (body == null) throw new ApiException(); + FileOutputStream outputStream = new FileOutputStream(file); + copyAndClose(body.byteStream(), outputStream); + } + + @Override + public void deleteFile(MailboxProperties properties, + MailboxFolderId folderId, MailboxFileId fileId) + throws IOException, ApiException, TolerableFailureException { + String path = "/files/" + folderId + "/" + fileId; + Request request = getRequestBuilder(properties.getAuthToken()) + .delete() + .url(properties.getOnionAddress() + path) + .build(); + OkHttpClient client = httpClientProvider.get(); + Response response = client.newCall(request).execute(); + if (response.code() == 404) throw new TolerableFailureException(); + if (response.code() != 200) throw new ApiException(); + } + + @Override + public List getFolders(MailboxProperties properties) + throws IOException, ApiException { + if (!properties.isOwner()) throw new IllegalArgumentException(); + Response response = sendGetRequest(properties, "/folders"); + if (response.code() != 200) throw new ApiException(); + + ResponseBody body = response.body(); + if (body == null) throw new ApiException(); + try { + JsonNode node = mapper.readTree(body.string()); + ArrayNode filesNode = getArray(node, "folders"); + List list = new ArrayList<>(); + for (JsonNode fileNode : filesNode) { + if (!fileNode.isObject()) throw new ApiException(); + ObjectNode objectNode = (ObjectNode) fileNode; + JsonNode idNode = objectNode.get("id"); + if (idNode == null || !idNode.isTextual()) { + throw new ApiException(); + } + String id = idNode.asText(); + list.add(MailboxFolderId.fromString(id)); + } + return list; + } catch (JacksonException | InvalidMailboxIdException e) { + throw new ApiException(); + } + } + + /* Helper Functions */ + private Response sendGetRequest(MailboxProperties properties, String path) throws IOException { Request request = getRequestBuilder(properties.getAuthToken()) @@ -164,9 +261,29 @@ class MailboxApiImpl implements MailboxApi { return client.newCall(request).execute(); } - private Request.Builder getRequestBuilder(String token) { + private Response sendPostRequest(MailboxProperties properties, String path, + RequestBody body) throws IOException { + Request request = getRequestBuilder(properties.getAuthToken()) + .url(properties.getOnionAddress() + path) + .post(body) + .build(); + OkHttpClient client = httpClientProvider.get(); + return client.newCall(request).execute(); + } + + private Request.Builder getRequestBuilder(MailboxId token) { return new Request.Builder() .addHeader("Authorization", "Bearer " + token); } + /* JSON helpers */ + + private ArrayNode getArray(JsonNode node, String name) throws ApiException { + JsonNode arrayNode = node.get(name); + if (arrayNode == null || !arrayNode.isArray()) { + throw new ApiException(); + } + return (ArrayNode) arrayNode; + } + } 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 dbe62a73a..bfb9f085e 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 @@ -3,6 +3,8 @@ package org.briarproject.bramble.mailbox; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.mailbox.InvalidMailboxIdException; +import org.briarproject.bramble.api.mailbox.MailboxAuthToken; import org.briarproject.bramble.api.mailbox.MailboxProperties; import org.briarproject.bramble.api.mailbox.MailboxSettingsManager; import org.briarproject.bramble.api.mailbox.MailboxStatus; @@ -43,7 +45,12 @@ class MailboxSettingsManagerImpl implements MailboxSettingsManager { String onion = s.get(SETTINGS_KEY_ONION); String token = s.get(SETTINGS_KEY_TOKEN); if (isNullOrEmpty(onion) || isNullOrEmpty(token)) return null; - return new MailboxProperties(onion, token, true); + try { + MailboxAuthToken tokenId = MailboxAuthToken.fromString(token); + return new MailboxProperties(onion, tokenId, true); + } catch (InvalidMailboxIdException e) { + throw new DbException(e); + } } @Override @@ -51,7 +58,7 @@ class MailboxSettingsManagerImpl implements MailboxSettingsManager { throws DbException { Settings s = new Settings(); s.put(SETTINGS_KEY_ONION, p.getOnionAddress()); - s.put(SETTINGS_KEY_TOKEN, p.getAuthToken()); + s.put(SETTINGS_KEY_TOKEN, p.getAuthToken().toString()); settingsManager.mergeSettings(txn, s, SETTINGS_NAMESPACE); } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxApiTest.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxApiTest.java index c27e7ef42..3f6cbafd3 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxApiTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/MailboxApiTest.java @@ -1,15 +1,26 @@ package org.briarproject.bramble.mailbox; +import com.fasterxml.jackson.databind.ObjectMapper; + import org.briarproject.bramble.api.WeakSingletonProvider; import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.mailbox.MailboxAuthToken; +import org.briarproject.bramble.api.mailbox.MailboxFileId; +import org.briarproject.bramble.api.mailbox.MailboxFolderId; +import org.briarproject.bramble.api.mailbox.MailboxId; import org.briarproject.bramble.api.mailbox.MailboxProperties; import org.briarproject.bramble.mailbox.MailboxApi.ApiException; import org.briarproject.bramble.mailbox.MailboxApi.MailboxContact; +import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile; import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException; import org.briarproject.bramble.test.BrambleTestCase; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import java.io.File; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import javax.annotation.Nonnull; @@ -19,12 +30,17 @@ import okhttp3.OkHttpClient; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; +import okio.Buffer; import static java.util.Collections.singletonList; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.briarproject.bramble.test.TestUtils.getContactId; -import static org.briarproject.bramble.test.TestUtils.getMailboxSecret; +import static org.briarproject.bramble.test.TestUtils.getRandomBytes; +import static org.briarproject.bramble.test.TestUtils.getRandomId; +import static org.briarproject.bramble.test.TestUtils.readBytes; +import static org.briarproject.bramble.test.TestUtils.writeBytes; import static org.briarproject.bramble.util.StringUtils.getRandomString; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -33,6 +49,9 @@ import static org.junit.Assert.assertTrue; public class MailboxApiTest extends BrambleTestCase { + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + private final OkHttpClient client = new OkHttpClient.Builder() .socketFactory(SocketFactory.getDefault()) .connectTimeout(60_000, MILLISECONDS) @@ -47,12 +66,15 @@ public class MailboxApiTest extends BrambleTestCase { }; private final MailboxApiImpl api = new MailboxApiImpl(httpClientProvider); - private final String token = getMailboxSecret(); - private final String token2 = getMailboxSecret(); + private final MailboxAuthToken token = new MailboxAuthToken(getRandomId()); + private final MailboxAuthToken token2 = new MailboxAuthToken(getRandomId()); private final ContactId contactId = getContactId(); - private final String contactToken = getMailboxSecret(); - private final String contactInboxId = getMailboxSecret(); - private final String contactOutboxId = getMailboxSecret(); + private final MailboxAuthToken contactToken = + new MailboxAuthToken(getRandomId()); + private final MailboxFolderId contactInboxId = + new MailboxFolderId(getRandomId()); + private final MailboxFolderId contactOutboxId = + new MailboxFolderId(getRandomId()); private final MailboxContact mailboxContact = new MailboxContact( contactId, contactToken, contactInboxId, contactOutboxId); @@ -370,12 +392,362 @@ public class MailboxApiTest extends BrambleTestCase { ); } + @Test + public void testAddFile() throws Exception { + File file = folder.newFile(); + byte[] bytes = getRandomBytes(1337); + writeBytes(file, bytes); + + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse()); + server.enqueue(new MockResponse().setResponseCode(401)); + server.enqueue(new MockResponse().setResponseCode(500)); + server.start(); + String baseUrl = getBaseUrl(server); + MailboxProperties properties = + new MailboxProperties(baseUrl, token, true); + + // file gets uploaded as expected + api.addFile(properties, contactInboxId, file); + RecordedRequest request1 = server.takeRequest(); + assertEquals("/files/" + contactInboxId, request1.getPath()); + assertEquals("POST", request1.getMethod()); + assertToken(request1, token); + assertArrayEquals(bytes, request1.getBody().readByteArray()); + + // request is not successful + assertThrows(ApiException.class, () -> + api.addFile(properties, contactInboxId, file)); + RecordedRequest request2 = server.takeRequest(); + assertEquals("/files/" + contactInboxId, request2.getPath()); + assertEquals("POST", request1.getMethod()); + assertToken(request2, token); + + // server error + assertThrows(ApiException.class, () -> + api.addFile(properties, contactInboxId, file)); + RecordedRequest request3 = server.takeRequest(); + assertEquals("/files/" + contactInboxId, request3.getPath()); + assertEquals("POST", request1.getMethod()); + assertToken(request3, token); + } + + @Test + public void testGetFiles() throws Exception { + MailboxFile mailboxFile1 = + new MailboxFile(new MailboxFileId(getRandomId()), 1337); + MailboxFile mailboxFile2 = + new MailboxFile(new MailboxFileId(getRandomId()), + System.currentTimeMillis()); + String fileResponse1 = + new ObjectMapper().writeValueAsString(mailboxFile1); + String fileResponse2 = + new ObjectMapper().writeValueAsString(mailboxFile2); + String validResponse1 = "{\"files\": [" + fileResponse1 + "] }"; + String validResponse2 = "{\"files\": [" + fileResponse1 + ", " + + fileResponse2 + "] }"; + String invalidResponse1 = "{\"files\":\"bar\"}"; + String invalidResponse2 = "{\"files\":{\"foo\":\"bar\"}}"; + String invalidResponse3 = "{\"files\": [" + fileResponse1 + ", 1] }"; + String invalidResponse4 = "{\"contacts\": [ 1, 2 ] }"; + + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody(validResponse1)); + server.enqueue(new MockResponse().setBody(validResponse2)); + server.enqueue(new MockResponse()); + server.enqueue(new MockResponse().setBody(invalidResponse1)); + server.enqueue(new MockResponse().setBody(invalidResponse2)); + server.enqueue(new MockResponse().setBody(invalidResponse3)); + server.enqueue(new MockResponse().setBody(invalidResponse4)); + server.enqueue(new MockResponse().setResponseCode(401)); + server.enqueue(new MockResponse().setResponseCode(500)); + server.start(); + String baseUrl = getBaseUrl(server); + MailboxProperties properties = + new MailboxProperties(baseUrl, token, true); + + // valid response with one file + List received1 = api.getFiles(properties, contactInboxId); + assertEquals(1, received1.size()); + assertEquals(mailboxFile1.name, received1.get(0).name); + assertEquals(mailboxFile1.time, received1.get(0).time); + RecordedRequest request1 = server.takeRequest(); + assertEquals("/files/" + contactInboxId, request1.getPath()); + assertEquals("GET", request1.getMethod()); + assertToken(request1, token); + + // valid response with two files + List received2 = api.getFiles(properties, contactInboxId); + assertEquals(2, received2.size()); + assertEquals(mailboxFile1.name, received2.get(0).name); + assertEquals(mailboxFile1.time, received2.get(0).time); + assertEquals(mailboxFile2.name, received2.get(1).name); + assertEquals(mailboxFile2.time, received2.get(1).time); + RecordedRequest request2 = server.takeRequest(); + assertEquals("/files/" + contactInboxId, request1.getPath()); + assertEquals("GET", request2.getMethod()); + assertToken(request2, token); + + // empty body + assertThrows(ApiException.class, () -> + api.getFiles(properties, contactInboxId)); + RecordedRequest request3 = server.takeRequest(); + assertEquals("/files/" + contactInboxId, request3.getPath()); + assertEquals("GET", request3.getMethod()); + assertToken(request3, token); + + // invalid response: string instead of list + assertThrows(ApiException.class, () -> + api.getFiles(properties, contactInboxId)); + RecordedRequest request4 = server.takeRequest(); + assertEquals("/files/" + contactInboxId, request4.getPath()); + assertEquals("GET", request4.getMethod()); + assertToken(request4, token); + + // invalid response: object instead of list + assertThrows(ApiException.class, () -> + api.getFiles(properties, contactInboxId)); + RecordedRequest request5 = server.takeRequest(); + assertEquals("/files/" + contactInboxId, request5.getPath()); + assertEquals("GET", request5.getMethod()); + assertToken(request5, token); + + // invalid response: list with non-objects + assertThrows(ApiException.class, () -> + api.getFiles(properties, contactInboxId)); + RecordedRequest request6 = server.takeRequest(); + assertEquals("/files/" + contactInboxId, request6.getPath()); + assertEquals("GET", request6.getMethod()); + assertToken(request6, token); + + // no files key in root object + assertThrows(ApiException.class, () -> + api.getFiles(properties, contactInboxId)); + RecordedRequest request7 = server.takeRequest(); + assertEquals("/files/" + contactInboxId, request7.getPath()); + assertEquals("GET", request7.getMethod()); + assertToken(request7, token); + + // 401 not authorized + assertThrows(ApiException.class, () -> + api.getFiles(properties, contactInboxId)); + RecordedRequest request8 = server.takeRequest(); + assertEquals("/files/" + contactInboxId, request8.getPath()); + assertEquals("GET", request8.getMethod()); + assertToken(request8, token); + + // 500 internal server error + assertThrows(ApiException.class, + () -> api.getFiles(properties, contactInboxId)); + RecordedRequest request9 = server.takeRequest(); + assertEquals("/files/" + contactInboxId, request9.getPath()); + assertEquals("GET", request9.getMethod()); + assertToken(request9, token); + } + + @Test + public void testGetFile() throws Exception { + MailboxFileId name = new MailboxFileId(getRandomId()); + File file1 = folder.newFile(); + File file2 = folder.newFile(); + File file3 = folder.newFile(); + byte[] bytes = getRandomBytes(1337); + + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody(new Buffer().write(bytes))); + server.enqueue(new MockResponse().setResponseCode(401)); + server.enqueue(new MockResponse().setResponseCode(500)); + server.start(); + String baseUrl = getBaseUrl(server); + MailboxProperties properties = + new MailboxProperties(baseUrl, token, true); + + // file gets downloaded as expected + api.getFile(properties, contactOutboxId, name, file1); + RecordedRequest request1 = server.takeRequest(); + assertEquals("/files/" + contactOutboxId + "/" + name, + request1.getPath()); + assertEquals("GET", request1.getMethod()); + assertToken(request1, token); + assertArrayEquals(bytes, readBytes(file1)); + + // request is not successful + assertThrows(ApiException.class, () -> + api.getFile(properties, contactOutboxId, name, file2)); + RecordedRequest request2 = server.takeRequest(); + assertEquals("/files/" + contactOutboxId + "/" + name, + request2.getPath()); + assertEquals("GET", request1.getMethod()); + assertToken(request2, token); + assertEquals(0, readBytes(file2).length); + + // server error + assertThrows(ApiException.class, () -> + api.getFile(properties, contactOutboxId, name, file3)); + RecordedRequest request3 = server.takeRequest(); + assertEquals("/files/" + contactOutboxId + "/" + name, + request3.getPath()); + assertEquals("GET", request1.getMethod()); + assertToken(request3, token); + assertEquals(0, readBytes(file3).length); + } + + @Test + public void testDeleteFile() throws Exception { + MailboxFileId name = new MailboxFileId(getRandomId()); + + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse()); + server.enqueue(new MockResponse().setResponseCode(205)); + server.enqueue(new MockResponse().setResponseCode(401)); + server.enqueue(new MockResponse().setResponseCode(404)); + server.start(); + String baseUrl = getBaseUrl(server); + MailboxProperties properties = + new MailboxProperties(baseUrl, token, true); + + // file gets deleted as expected + api.deleteFile(properties, contactInboxId, name); + RecordedRequest request1 = server.takeRequest(); + assertEquals("DELETE", request1.getMethod()); + assertEquals("/files/" + contactInboxId + "/" + name, + request1.getPath()); + assertToken(request1, token); + + // request is not returning 200 + assertThrows(ApiException.class, () -> + api.deleteFile(properties, contactInboxId, name)); + RecordedRequest request2 = server.takeRequest(); + assertEquals("DELETE", request2.getMethod()); + assertEquals("/files/" + contactInboxId + "/" + name, + request2.getPath()); + assertToken(request2, token); + + // request is not authorized + assertThrows(ApiException.class, () -> + api.deleteFile(properties, contactInboxId, name)); + RecordedRequest request3 = server.takeRequest(); + assertEquals("DELETE", request3.getMethod()); + assertEquals("/files/" + contactInboxId + "/" + name, + request3.getPath()); + assertToken(request3, token); + + // file not found is tolerable + assertThrows(TolerableFailureException.class, () -> + api.deleteFile(properties, contactInboxId, name)); + RecordedRequest request4 = server.takeRequest(); + assertEquals("DELETE", request4.getMethod()); + assertEquals("/files/" + contactInboxId + "/" + name, + request4.getPath()); + assertToken(request4, token); + } + + @Test + public void testGetFolders() throws Exception { + MailboxFolderId id1 = new MailboxFolderId(getRandomId()); + MailboxFolderId id2 = new MailboxFolderId(getRandomId()); + String validResponse1 = "{\"folders\": [ {\"id\": \"" + id1 + "\"} ] }"; + String validResponse2 = "{\"folders\": [ {\"id\": \"" + id1 + "\"}, " + + "{ \"id\": \"" + id2 + "\"} ] }"; + String invalidResponse1 = "{\"folders\":\"bar\"}"; + String invalidResponse2 = "{\"folders\":{\"foo\":\"bar\"}}"; + String invalidResponse3 = + "{\"folders\": [ {\"id\": \"" + id1 + "\", 1] }"; + String invalidResponse4 = "{\"files\": [ 1, 2 ] }"; + + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody(validResponse1)); + server.enqueue(new MockResponse().setBody(validResponse2)); + server.enqueue(new MockResponse()); + server.enqueue(new MockResponse().setBody(invalidResponse1)); + server.enqueue(new MockResponse().setBody(invalidResponse2)); + server.enqueue(new MockResponse().setBody(invalidResponse3)); + server.enqueue(new MockResponse().setBody(invalidResponse4)); + server.enqueue(new MockResponse().setResponseCode(401)); + server.enqueue(new MockResponse().setResponseCode(500)); + server.start(); + String baseUrl = getBaseUrl(server); + MailboxProperties properties = + new MailboxProperties(baseUrl, token, true); + + // valid response with one folders + assertEquals(singletonList(id1), api.getFolders(properties)); + RecordedRequest request1 = server.takeRequest(); + assertEquals("/folders", request1.getPath()); + assertEquals("GET", request1.getMethod()); + assertToken(request1, token); + + // valid response with two folders + assertEquals(Arrays.asList(id1, id2), api.getFolders(properties)); + RecordedRequest request2 = server.takeRequest(); + assertEquals("/folders", request1.getPath()); + assertEquals("GET", request2.getMethod()); + assertToken(request2, token); + + // empty body + assertThrows(ApiException.class, () -> api.getFolders(properties)); + RecordedRequest request3 = server.takeRequest(); + assertEquals("/folders", request3.getPath()); + assertEquals("GET", request3.getMethod()); + assertToken(request3, token); + + // invalid response: string instead of list + assertThrows(ApiException.class, () -> api.getFolders(properties)); + RecordedRequest request4 = server.takeRequest(); + assertEquals("/folders", request4.getPath()); + assertEquals("GET", request4.getMethod()); + assertToken(request4, token); + + // invalid response: object instead of list + assertThrows(ApiException.class, () -> api.getFolders(properties)); + RecordedRequest request5 = server.takeRequest(); + assertEquals("/folders", request5.getPath()); + assertEquals("GET", request5.getMethod()); + assertToken(request5, token); + + // invalid response: list with non-objects + assertThrows(ApiException.class, () -> api.getFolders(properties)); + RecordedRequest request6 = server.takeRequest(); + assertEquals("/folders", request6.getPath()); + assertEquals("GET", request6.getMethod()); + assertToken(request6, token); + + // no folders key in root object + assertThrows(ApiException.class, () -> api.getFolders(properties)); + RecordedRequest request7 = server.takeRequest(); + assertEquals("/folders", request7.getPath()); + assertEquals("GET", request7.getMethod()); + assertToken(request7, token); + + // 401 not authorized + assertThrows(ApiException.class, () -> api.getFolders(properties)); + RecordedRequest request8 = server.takeRequest(); + assertEquals("/folders", request8.getPath()); + assertEquals("GET", request8.getMethod()); + assertToken(request8, token); + + // 500 internal server error + assertThrows(ApiException.class, () -> api.getFolders(properties)); + RecordedRequest request9 = server.takeRequest(); + assertEquals("/folders", request9.getPath()); + assertEquals("GET", request9.getMethod()); + assertToken(request9, token); + } + + @Test + public void testGetFoldersOnlyForOwner() { + MailboxProperties properties = + new MailboxProperties("", token, false); + assertThrows(IllegalArgumentException.class, () -> + api.getFolders(properties)); + } + private String getBaseUrl(MockWebServer server) { String baseUrl = server.url("").toString(); return baseUrl.substring(0, baseUrl.length() - 1); } - private void assertToken(RecordedRequest request, String token) { + private void assertToken(RecordedRequest request, MailboxId token) { assertNotNull(request.getHeader("Authorization")); assertEquals("Bearer " + token, request.getHeader("Authorization")); } 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 19fa9a64c..eccb311ed 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 @@ -2,17 +2,26 @@ package org.briarproject.bramble.mailbox; import org.briarproject.bramble.api.WeakSingletonProvider; import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.mailbox.InvalidMailboxIdException; +import org.briarproject.bramble.api.mailbox.MailboxAuthToken; +import org.briarproject.bramble.api.mailbox.MailboxFileId; +import org.briarproject.bramble.api.mailbox.MailboxFolderId; import org.briarproject.bramble.api.mailbox.MailboxProperties; import org.briarproject.bramble.mailbox.MailboxApi.ApiException; import org.briarproject.bramble.mailbox.MailboxApi.MailboxContact; +import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile; import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException; import org.briarproject.bramble.test.BrambleTestCase; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import java.io.File; import java.io.IOException; import java.util.Arrays; +import java.util.List; import javax.annotation.Nonnull; import javax.net.SocketFactory; @@ -22,8 +31,12 @@ import okhttp3.OkHttpClient; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.briarproject.bramble.test.TestUtils.getMailboxSecret; +import static org.briarproject.bramble.test.TestUtils.getRandomBytes; +import static org.briarproject.bramble.test.TestUtils.getRandomId; import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled; +import static org.briarproject.bramble.test.TestUtils.readBytes; +import static org.briarproject.bramble.test.TestUtils.writeBytes; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -31,9 +44,20 @@ import static org.junit.Assume.assumeTrue; public class MailboxIntegrationTest extends BrambleTestCase { + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + private final static String URL_BASE = "http://127.0.0.1:8000"; - private final static String SETUP_TOKEN = - "54686973206973206120736574757020746f6b656e20666f722042726961722e"; + private final static MailboxAuthToken SETUP_TOKEN; + + static { + try { + SETUP_TOKEN = MailboxAuthToken.fromString( + "54686973206973206120736574757020746f6b656e20666f722042726961722e"); + } catch (InvalidMailboxIdException e) { + throw new IllegalStateException(); + } + } private final OkHttpClient client = new OkHttpClient.Builder() .socketFactory(SocketFactory.getDefault()) @@ -64,7 +88,7 @@ public class MailboxIntegrationTest extends BrambleTestCase { if (ownerProperties != null) return; MailboxProperties setupProperties = new MailboxProperties(URL_BASE, SETUP_TOKEN, true); - String ownerToken = api.setup(setupProperties); + MailboxAuthToken ownerToken = api.setup(setupProperties); ownerProperties = new MailboxProperties(URL_BASE, ownerToken, true); } @@ -101,9 +125,128 @@ public class MailboxIntegrationTest extends BrambleTestCase { () -> api.deleteContact(ownerProperties, contactId2)); } + @Test + public void testFileManagementApi() throws Exception { + // add contact, so we can leave each other files + ContactId contactId = new ContactId(1); + MailboxContact contact = getMailboxContact(contactId); + MailboxProperties contactProperties = new MailboxProperties( + ownerProperties.getOnionAddress(), contact.token, false); + api.addContact(ownerProperties, contact); + + // upload a file for our contact + File file1 = folder.newFile(); + byte[] bytes1 = getRandomBytes(2048); + writeBytes(file1, bytes1); + api.addFile(ownerProperties, contact.inboxId, file1); + + // contact checks files + List files1 = + api.getFiles(contactProperties, contact.inboxId); + assertEquals(1, files1.size()); + MailboxFileId fileName1 = files1.get(0).name; + + // owner can't check files + assertThrows(ApiException.class, () -> + api.getFiles(ownerProperties, contact.inboxId)); + + // contact downloads file + File file1downloaded = folder.newFile(); + api.getFile(contactProperties, contact.inboxId, fileName1, + file1downloaded); + assertArrayEquals(bytes1, readBytes(file1downloaded)); + + // owner can't download file, even if knowing name + File file1forbidden = folder.newFile(); + assertThrows(ApiException.class, () -> api.getFile(ownerProperties, + contact.inboxId, fileName1, file1forbidden)); + assertEquals(0, file1forbidden.length()); + + // owner can't delete file + assertThrows(ApiException.class, () -> + api.deleteFile(ownerProperties, contact.inboxId, fileName1)); + + // contact deletes file + api.deleteFile(contactProperties, contact.inboxId, fileName1); + assertEquals(0, + api.getFiles(contactProperties, contact.inboxId).size()); + + // contact uploads two files for the owner + File file2 = folder.newFile(); + File file3 = folder.newFile(); + byte[] bytes2 = getRandomBytes(2048); + byte[] bytes3 = getRandomBytes(1024); + writeBytes(file2, bytes2); + writeBytes(file3, bytes3); + api.addFile(contactProperties, contact.outboxId, file2); + api.addFile(contactProperties, contact.outboxId, file3); + + // owner checks folders with available files + List folders = api.getFolders(ownerProperties); + assertEquals(singletonList(contact.outboxId), folders); + + // owner lists files in contact's outbox + List files2 = + api.getFiles(ownerProperties, contact.outboxId); + assertEquals(2, files2.size()); + MailboxFileId file2name = files2.get(0).name; + MailboxFileId file3name = files2.get(1).name; + + // contact can't list files in contact's outbox + assertThrows(ApiException.class, () -> + api.getFiles(contactProperties, contact.outboxId)); + + // owner downloads both files from contact's outbox + File file2downloaded = folder.newFile(); + File file3downloaded = folder.newFile(); + api.getFile(ownerProperties, contact.outboxId, file2name, + file2downloaded); + api.getFile(ownerProperties, contact.outboxId, file3name, + file3downloaded); + byte[] downloadedBytes2 = readBytes(file2downloaded); + byte[] downloadedBytes3 = readBytes(file3downloaded); + // file order is preserved (sorted by time), + // so we know what file is which + assertArrayEquals(bytes2, downloadedBytes2); + assertArrayEquals(bytes3, downloadedBytes3); + + // contact can't download files again, even if knowing name + File file2forbidden = folder.newFile(); + File file3forbidden = folder.newFile(); + assertThrows(ApiException.class, () -> api.getFile(contactProperties, + contact.outboxId, file2name, file2forbidden)); + assertThrows(ApiException.class, () -> api.getFile(contactProperties, + contact.outboxId, file3name, file3forbidden)); + assertEquals(0, file1forbidden.length()); + assertEquals(0, file2forbidden.length()); + + // contact can't delete files in outbox + assertThrows(ApiException.class, () -> + api.deleteFile(contactProperties, contact.outboxId, file2name)); + assertThrows(ApiException.class, () -> + api.deleteFile(contactProperties, contact.outboxId, file3name)); + + // owner deletes files + api.deleteFile(ownerProperties, contact.outboxId, file2name); + api.deleteFile(ownerProperties, contact.outboxId, file3name); + assertEquals(emptyList(), + api.getFiles(ownerProperties, contact.outboxId)); + assertEquals(emptyList(), api.getFolders(ownerProperties)); + + // deleting a non-existent file is tolerable + assertThrows(TolerableFailureException.class, () -> + api.deleteFile(ownerProperties, contact.outboxId, file3name)); + + // owner deletes contact again to leave clean state for other tests + api.deleteContact(ownerProperties, contactId); + assertEquals(emptyList(), api.getContacts(ownerProperties)); + } + private MailboxContact getMailboxContact(ContactId contactId) { - return new MailboxContact(contactId, getMailboxSecret(), - getMailboxSecret(), getMailboxSecret()); + MailboxAuthToken authToken = new MailboxAuthToken(getRandomId()); + MailboxFolderId inboxId = new MailboxFolderId(getRandomId()); + MailboxFolderId outboxId = new MailboxFolderId(getRandomId()); + return new MailboxContact(contactId, authToken, inboxId, outboxId); } } 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 424554c97..57aa8894a 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 @@ -2,6 +2,7 @@ package org.briarproject.bramble.mailbox; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.mailbox.MailboxAuthToken; import org.briarproject.bramble.api.mailbox.MailboxProperties; import org.briarproject.bramble.api.mailbox.MailboxSettingsManager; import org.briarproject.bramble.api.mailbox.MailboxStatus; @@ -20,6 +21,7 @@ import static org.briarproject.bramble.mailbox.MailboxSettingsManagerImpl.SETTIN import static org.briarproject.bramble.mailbox.MailboxSettingsManagerImpl.SETTINGS_KEY_TOKEN; import static org.briarproject.bramble.mailbox.MailboxSettingsManagerImpl.SETTINGS_NAMESPACE; import static org.briarproject.bramble.mailbox.MailboxSettingsManagerImpl.SETTINGS_UPLOADS_NAMESPACE; +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.assertNotNull; @@ -35,7 +37,7 @@ public class MailboxSettingsManagerImplTest extends BrambleMockTestCase { new MailboxSettingsManagerImpl(settingsManager); private final Random random = new Random(); private final String onion = getRandomString(64); - private final String token = getRandomString(64); + private final MailboxAuthToken token = new MailboxAuthToken(getRandomId()); private final ContactId contactId1 = new ContactId(random.nextInt()); private final ContactId contactId2 = new ContactId(random.nextInt()); private final ContactId contactId3 = new ContactId(random.nextInt()); @@ -62,7 +64,7 @@ public class MailboxSettingsManagerImplTest extends BrambleMockTestCase { Transaction txn = new Transaction(null, true); Settings settings = new Settings(); settings.put(SETTINGS_KEY_ONION, onion); - settings.put(SETTINGS_KEY_TOKEN, token); + settings.put(SETTINGS_KEY_TOKEN, token.toString()); context.checking(new Expectations() {{ oneOf(settingsManager).getSettings(txn, SETTINGS_NAMESPACE); @@ -81,7 +83,7 @@ public class MailboxSettingsManagerImplTest extends BrambleMockTestCase { Transaction txn = new Transaction(null, false); Settings expectedSettings = new Settings(); expectedSettings.put(SETTINGS_KEY_ONION, onion); - expectedSettings.put(SETTINGS_KEY_TOKEN, token); + expectedSettings.put(SETTINGS_KEY_TOKEN, token.toString()); MailboxProperties properties = new MailboxProperties(onion, token, true); @@ -180,7 +182,7 @@ public class MailboxSettingsManagerImplTest extends BrambleMockTestCase { Transaction txn = new Transaction(null, true); Settings settings = new Settings(); settings.put(String.valueOf(contactId1.getInt()), onion); - settings.put(String.valueOf(contactId2.getInt()), token); + settings.put(String.valueOf(contactId2.getInt()), token.toString()); settings.put(String.valueOf(contactId3.getInt()), ""); context.checking(new Expectations() {{ @@ -192,7 +194,8 @@ public class MailboxSettingsManagerImplTest extends BrambleMockTestCase { String filename1 = manager.getPendingUpload(txn, contactId1); assertEquals(onion, filename1); String filename2 = manager.getPendingUpload(txn, contactId2); - assertEquals(token, filename2); + assertNotNull(filename2); + assertEquals(token.toString(), filename2); String filename3 = manager.getPendingUpload(txn, contactId3); assertNull(filename3); String filename4 =