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 3e171904e..3fd19912a 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 @@ -31,6 +31,7 @@ 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,6 +47,7 @@ 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.StringUtils.getRandomString; +import static org.briarproject.bramble.util.StringUtils.toHexString; public class TestUtils { @@ -209,6 +211,10 @@ public class TestUtils { getAgreementPublicKey(), verified); } + public static String getMailboxSecret() { + return toHexString(getRandomBytes(32)).toLowerCase(Locale.US); + } + 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 6c5999990..34a740c3e 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 @@ -1,5 +1,6 @@ package org.briarproject.bramble.mailbox; +import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.mailbox.MailboxProperties; import java.io.IOException; @@ -13,21 +14,55 @@ interface MailboxApi { * * @param properties MailboxProperties with the setup token * @return the owner token - * @throws PermanentFailureException for 401 response. + * @throws ApiException for 401 response. */ String setup(MailboxProperties properties) - throws IOException, PermanentFailureException; + throws IOException, ApiException; /** * Checks the status of the mailbox. * * @return true if the status is OK, false otherwise. - * @throws PermanentFailureException for 401 response. + * @throws ApiException for 401 response. */ boolean checkStatus(MailboxProperties properties) - throws IOException, PermanentFailureException; + throws IOException, ApiException; + + /** + * Adds a new contact to the mailbox. + * + * @throws TolerableFailureException if response code is 409 + * (contact was already added). + */ + void addContact(MailboxProperties properties, MailboxContact contact) + throws IOException, ApiException, + TolerableFailureException; @Immutable - class PermanentFailureException extends Exception { + class MailboxContact { + public final int contactId; + public final String token, inboxId, outboxId; + + MailboxContact(ContactId contactId, + String token, + String inboxId, + String outboxId) { + this.contactId = contactId.getInt(); + this.token = token; + this.inboxId = inboxId; + this.outboxId = outboxId; + } + } + + @Immutable + class ApiException extends Exception { + } + + /** + * A failure that does not need to be retried, + * e.g. when adding a contact that already exists. + */ + @Immutable + class TolerableFailureException 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 3c0906fb5..218283a42 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 @@ -12,12 +12,15 @@ import java.io.IOException; import javax.inject.Inject; +import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.Response; 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; @@ -28,6 +31,8 @@ class MailboxApiImpl implements MailboxApi { private final JsonMapper mapper = JsonMapper.builder() .enable(BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES) .build(); + private static final MediaType JSON = + requireNonNull(MediaType.parse("application/json; charset=utf-8")); @Inject MailboxApiImpl(WeakSingletonProvider httpClientProvider) { @@ -36,7 +41,7 @@ class MailboxApiImpl implements MailboxApi { @Override public String setup(MailboxProperties properties) - throws IOException, PermanentFailureException { + throws IOException, ApiException { if (!properties.isOwner()) throw new IllegalArgumentException(); Request request = getRequestBuilder(properties.getAuthToken()) .url(properties.getOnionAddress() + "/setup") @@ -45,23 +50,23 @@ class MailboxApiImpl implements MailboxApi { 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 PermanentFailureException(); - if (!response.isSuccessful()) throw new PermanentFailureException(); + if (response.code() == 401) throw new ApiException(); + if (!response.isSuccessful()) throw new ApiException(); ResponseBody body = response.body(); - if (body == null) throw new PermanentFailureException(); + if (body == null) throw new ApiException(); try { JsonNode node = mapper.readTree(body.string()); JsonNode tokenNode = node.get("token"); if (tokenNode == null) { - throw new PermanentFailureException(); + throw new ApiException(); } String ownerToken = tokenNode.textValue(); if (ownerToken == null || !isValidToken(ownerToken)) { - throw new PermanentFailureException(); + throw new ApiException(); } return ownerToken; } catch (JacksonException e) { - throw new PermanentFailureException(); + throw new ApiException(); } } @@ -78,17 +83,34 @@ class MailboxApiImpl implements MailboxApi { @Override public boolean checkStatus(MailboxProperties properties) - throws IOException, PermanentFailureException { + throws IOException, ApiException { if (!properties.isOwner()) throw new IllegalArgumentException(); Request request = getRequestBuilder(properties.getAuthToken()) .url(properties.getOnionAddress() + "/status") .build(); OkHttpClient client = httpClientProvider.get(); Response response = client.newCall(request).execute(); - if (response.code() == 401) throw new PermanentFailureException(); + if (response.code() == 401) throw new ApiException(); return response.isSuccessful(); } + @Override + public void addContact(MailboxProperties properties, MailboxContact contact) + 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(); + if (response.code() == 409) throw new TolerableFailureException(); + if (!response.isSuccessful()) throw new ApiException(); + } + 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 c74570a62..5f4399ef3 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,8 +1,11 @@ package org.briarproject.bramble.mailbox; import org.briarproject.bramble.api.WeakSingletonProvider; +import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.mailbox.MailboxProperties; -import org.briarproject.bramble.mailbox.MailboxApi.PermanentFailureException; +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.Test; @@ -15,9 +18,9 @@ import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static org.briarproject.bramble.test.TestUtils.getRandomBytes; +import static org.briarproject.bramble.test.TestUtils.getContactId; +import static org.briarproject.bramble.test.TestUtils.getMailboxSecret; import static org.briarproject.bramble.util.StringUtils.getRandomString; -import static org.briarproject.bramble.util.StringUtils.toHexString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -40,8 +43,14 @@ public class MailboxApiTest extends BrambleTestCase { }; private final MailboxApiImpl api = new MailboxApiImpl(httpClientProvider); - private final String token = toHexString(getRandomBytes(32)); - private final String token2 = toHexString(getRandomBytes(32)); + private final String token = getMailboxSecret(); + private final String token2 = getMailboxSecret(); + private final ContactId contactId = getContactId(); + private final String contactToken = getMailboxSecret(); + private final String contactInboxId = getMailboxSecret(); + private final String contactOutboxId = getMailboxSecret(); + private final MailboxContact mailboxContact = new MailboxContact( + contactId, contactToken, contactInboxId, contactOutboxId); @Test public void testSetup() throws Exception { @@ -74,50 +83,42 @@ public class MailboxApiTest extends BrambleTestCase { assertToken(request1, token); // empty body - assertThrows(PermanentFailureException.class, - () -> api.setup(properties)); + assertThrows(ApiException.class, () -> api.setup(properties)); RecordedRequest request2 = server.takeRequest(); assertEquals("/setup", request2.getPath()); assertEquals("PUT", request2.getMethod()); assertToken(request2, token); // invalid response - assertThrows(PermanentFailureException.class, - () -> api.setup(properties)); + assertThrows(ApiException.class, () -> api.setup(properties)); RecordedRequest request3 = server.takeRequest(); assertEquals("/setup", request3.getPath()); assertEquals("PUT", request3.getMethod()); assertToken(request3, token); // 401 response - assertThrows(PermanentFailureException.class, - () -> api.setup(properties2)); + assertThrows(ApiException.class, () -> api.setup(properties2)); RecordedRequest request4 = server.takeRequest(); assertEquals("/setup", request4.getPath()); assertEquals("PUT", request4.getMethod()); assertToken(request4, token2); // 500 response - assertThrows(PermanentFailureException.class, - () -> api.setup(properties)); + assertThrows(ApiException.class, () -> api.setup(properties)); RecordedRequest request5 = server.takeRequest(); assertEquals("/setup", request5.getPath()); assertEquals("PUT", request5.getMethod()); assertToken(request5, token); // invalid json dict token response - assertThrows(PermanentFailureException.class, - () -> api.setup(properties) - ); + assertThrows(ApiException.class, () -> api.setup(properties)); RecordedRequest request6 = server.takeRequest(); assertEquals("/setup", request6.getPath()); assertEquals("PUT", request6.getMethod()); assertToken(request6, token); // invalid non-hex string token response - assertThrows(PermanentFailureException.class, - () -> api.setup(properties) - ); + assertThrows(ApiException.class, () -> api.setup(properties)); RecordedRequest request7 = server.takeRequest(); assertEquals("/setup", request7.getPath()); assertEquals("PUT", request7.getMethod()); @@ -152,9 +153,7 @@ public class MailboxApiTest extends BrambleTestCase { assertEquals("/status", request1.getPath()); assertToken(request1, token); - assertThrows(PermanentFailureException.class, () -> - api.checkStatus(properties2) - ); + assertThrows(ApiException.class, () -> api.checkStatus(properties2)); RecordedRequest request2 = server.takeRequest(); assertEquals("/status", request2.getPath()); assertToken(request2, token2); @@ -175,6 +174,52 @@ public class MailboxApiTest extends BrambleTestCase { ); } + @Test + public void testAddContact() throws Exception { + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse()); + server.enqueue(new MockResponse().setResponseCode(401)); + server.enqueue(new MockResponse().setResponseCode(409)); + server.start(); + String baseUrl = getBaseUrl(server); + MailboxProperties properties = + new MailboxProperties(baseUrl, token, true); + + // contact gets added as expected + api.addContact(properties, mailboxContact); + RecordedRequest request1 = server.takeRequest(); + assertEquals("/contacts", request1.getPath()); + assertToken(request1, token); + String expected = "{\"contactId\":" + contactId.getInt() + + ",\"token\":\"" + contactToken + + "\",\"inboxId\":\"" + contactInboxId + + "\",\"outboxId\":\"" + contactOutboxId + + "\"}"; + assertEquals(expected, request1.getBody().readUtf8()); + + // request is not successful + assertThrows(ApiException.class, () -> + api.addContact(properties, mailboxContact)); + RecordedRequest request2 = server.takeRequest(); + assertEquals("/contacts", request2.getPath()); + assertToken(request2, token); + + // contact already exists + assertThrows(TolerableFailureException.class, () -> + api.addContact(properties, mailboxContact)); + RecordedRequest request3 = server.takeRequest(); + assertEquals("/contacts", request3.getPath()); + assertToken(request3, token); + } + + @Test + public void testAddContactOnlyForOwner() { + MailboxProperties properties = + new MailboxProperties("", token, false); + assertThrows(IllegalArgumentException.class, () -> + api.addContact(properties, mailboxContact)); + } + private String getBaseUrl(MockWebServer server) { String baseUrl = server.url("").toString(); return baseUrl.substring(0, baseUrl.length() - 1);