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..862be0ca9 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,7 +25,11 @@ 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; @@ -46,6 +50,7 @@ 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; @@ -215,6 +220,24 @@ public class TestUtils { 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) { int size = samples.size(); if (size == 0) throw new IllegalArgumentException(); 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..73760208e 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 @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.mailbox.MailboxProperties; +import java.io.File; import java.io.IOException; import java.util.Collection; @@ -57,6 +58,16 @@ 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, String folderId, + File file) throws IOException, ApiException; + @Immutable @JsonSerialize class MailboxContact { 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..53b2e5836 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 @@ -9,6 +9,7 @@ import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.mailbox.MailboxProperties; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; @@ -37,6 +38,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) { @@ -94,6 +97,8 @@ class MailboxApiImpl implements MailboxApi { return response.isSuccessful(); } + /* Contact Management API (owner only) */ + @Override public void addContact(MailboxProperties properties, MailboxContact contact) throws IOException, ApiException, @@ -101,12 +106,7 @@ class MailboxApiImpl implements MailboxApi { 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(); } @@ -155,6 +155,18 @@ class MailboxApiImpl implements MailboxApi { } } + /* File Management (owner and contacts) */ + + @Override + public void addFile(MailboxProperties properties, String 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(); + } + /* Helper Functions */ + private Response sendGetRequest(MailboxProperties properties, String path) throws IOException { Request request = getRequestBuilder(properties.getAuthToken()) @@ -164,6 +176,16 @@ class MailboxApiImpl implements MailboxApi { return client.newCall(request).execute(); } + 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(String token) { return new Request.Builder() .addHeader("Authorization", "Bearer " + token); 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..3e88c2d8d 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 @@ -7,8 +7,11 @@ import org.briarproject.bramble.mailbox.MailboxApi.ApiException; import org.briarproject.bramble.mailbox.MailboxApi.MailboxContact; 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.List; @@ -24,7 +27,10 @@ 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.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 +39,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) @@ -370,6 +379,46 @@ 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); + } + private String getBaseUrl(MockWebServer server) { String baseUrl = server.url("").toString(); return baseUrl.substring(0, baseUrl.length() - 1);