Merge branch '1585-new-messaging-client' into 'master'

Add support for image attachments to messaging client

Closes #1585

See merge request briar/briar!1133
This commit is contained in:
Torsten Grote
2019-06-18 14:55:40 +00:00
48 changed files with 1363 additions and 261 deletions

View File

@@ -25,7 +25,10 @@ public interface ClientHelper {
throws DbException, FormatException; throws DbException, FormatException;
void addLocalMessage(Transaction txn, Message m, BdfDictionary metadata, void addLocalMessage(Transaction txn, Message m, BdfDictionary metadata,
boolean shared) throws DbException, FormatException; boolean shared, boolean temporary)
throws DbException, FormatException;
Message createMessage(GroupId g, long timestamp, byte[] body);
Message createMessage(GroupId g, long timestamp, BdfList body) Message createMessage(GroupId g, long timestamp, BdfList body)
throws FormatException; throws FormatException;
@@ -108,7 +111,7 @@ public interface ClientHelper {
Author parseAndValidateAuthor(BdfList author) throws FormatException; Author parseAndValidateAuthor(BdfList author) throws FormatException;
PublicKey parseAndValidateAgreementPublicKey(byte[] publicKeyBytes) PublicKey parseAndValidateAgreementPublicKey(byte[] publicKeyBytes)
throws FormatException; throws FormatException;
TransportProperties parseAndValidateTransportProperties( TransportProperties parseAndValidateTransportProperties(
BdfDictionary properties) throws FormatException; BdfDictionary properties) throws FormatException;

View File

@@ -85,15 +85,21 @@ class ClientHelperImpl implements ClientHelper {
@Override @Override
public void addLocalMessage(Message m, BdfDictionary metadata, public void addLocalMessage(Message m, BdfDictionary metadata,
boolean shared) throws DbException, FormatException { boolean shared) throws DbException, FormatException {
db.transaction(false, txn -> addLocalMessage(txn, m, metadata, shared)); db.transaction(false, txn -> addLocalMessage(txn, m, metadata, shared,
false));
} }
@Override @Override
public void addLocalMessage(Transaction txn, Message m, public void addLocalMessage(Transaction txn, Message m,
BdfDictionary metadata, boolean shared) BdfDictionary metadata, boolean shared, boolean temporary)
throws DbException, FormatException { throws DbException, FormatException {
db.addLocalMessage(txn, m, metadataEncoder.encode(metadata), shared, db.addLocalMessage(txn, m, metadataEncoder.encode(metadata), shared,
false); temporary);
}
@Override
public Message createMessage(GroupId g, long timestamp, byte[] body) {
return messageFactory.createMessage(g, timestamp, body);
} }
@Override @Override

View File

@@ -284,7 +284,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
meta.put("transportId", t.getString()); meta.put("transportId", t.getString());
meta.put("version", version); meta.put("version", version);
meta.put("local", local); meta.put("local", local);
clientHelper.addLocalMessage(txn, m, meta, shared); clientHelper.addLocalMessage(txn, m, meta, shared, false);
} catch (FormatException e) { } catch (FormatException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View File

@@ -314,6 +314,7 @@ class ValidationManagerImpl implements ValidationManager, Service,
try { try {
shareMsg = hook.incomingMessage(txn, m, meta); shareMsg = hook.incomingMessage(txn, m, meta);
} catch (InvalidMessageException e) { } catch (InvalidMessageException e) {
logException(LOG, INFO, e);
invalidateMessage(txn, m.getId()); invalidateMessage(txn, m.getId());
return new DeliveryResult(false, false); return new DeliveryResult(false, false);
} }

View File

@@ -438,7 +438,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
BdfDictionary meta = new BdfDictionary(); BdfDictionary meta = new BdfDictionary();
meta.put(MSG_KEY_UPDATE_VERSION, updateVersion); meta.put(MSG_KEY_UPDATE_VERSION, updateVersion);
meta.put(MSG_KEY_LOCAL, true); meta.put(MSG_KEY_LOCAL, true);
clientHelper.addLocalMessage(txn, m, meta, true); clientHelper.addLocalMessage(txn, m, meta, true, false);
} catch (FormatException e) { } catch (FormatException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View File

@@ -637,7 +637,8 @@ public class TransportPropertyManagerImplTest extends BrambleMockTestCase {
will(returnValue(timestamp)); will(returnValue(timestamp));
oneOf(clientHelper).createMessage(g, timestamp, body); oneOf(clientHelper).createMessage(g, timestamp, body);
will(returnValue(message)); will(returnValue(message));
oneOf(clientHelper).addLocalMessage(txn, message, meta, shared); oneOf(clientHelper).addLocalMessage(txn, message, meta, shared,
false);
}}); }});
} }
} }

View File

@@ -1,9 +1,11 @@
package org.briarproject.bramble.test; package org.briarproject.bramble.test;
import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.battery.DefaultBatteryManagerModule; import org.briarproject.bramble.battery.DefaultBatteryManagerModule;
import org.briarproject.bramble.event.DefaultEventExecutorModule; import org.briarproject.bramble.event.DefaultEventExecutorModule;
import dagger.Module; import dagger.Module;
import dagger.Provides;
@Module(includes = { @Module(includes = {
DefaultBatteryManagerModule.class, DefaultBatteryManagerModule.class,
@@ -13,4 +15,20 @@ import dagger.Module;
TestSecureRandomModule.class TestSecureRandomModule.class
}) })
public class BrambleCoreIntegrationTestModule { public class BrambleCoreIntegrationTestModule {
@Provides
FeatureFlags provideFeatureFlags() {
return new FeatureFlags() {
@Override
public boolean shouldEnableImageAttachments() {
return true;
}
@Override
public boolean shouldEnableRemoteContacts() {
return true;
}
};
}
} }

View File

@@ -131,7 +131,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
localUpdateBody); localUpdateBody);
will(returnValue(localUpdate)); will(returnValue(localUpdate));
oneOf(clientHelper).addLocalMessage(txn, localUpdate, oneOf(clientHelper).addLocalMessage(txn, localUpdate,
localUpdateMeta, true); localUpdateMeta, true, false);
}}); }});
} }
@@ -284,7 +284,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
newLocalUpdateBody); newLocalUpdateBody);
will(returnValue(newLocalUpdate)); will(returnValue(newLocalUpdate));
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate, oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true); newLocalUpdateMeta, true, false);
// No visibilities have changed // No visibilities have changed
}}); }});
@@ -382,7 +382,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
newLocalUpdateBody); newLocalUpdateBody);
will(returnValue(newLocalUpdate)); will(returnValue(newLocalUpdate));
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate, oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true); newLocalUpdateMeta, true, false);
// The client's visibility has changed // The client's visibility has changed
oneOf(hook).onClientVisibilityChanging(txn, contact, visibility); oneOf(hook).onClientVisibilityChanging(txn, contact, visibility);
}}); }});
@@ -567,7 +567,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
newLocalUpdateBody); newLocalUpdateBody);
will(returnValue(newLocalUpdate)); will(returnValue(newLocalUpdate));
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate, oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true); newLocalUpdateMeta, true, false);
// The client's visibility has changed // The client's visibility has changed
oneOf(clientHelper).getGroupMetadataAsDictionary(txn, oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
contactGroup.getId()); contactGroup.getId());
@@ -640,7 +640,7 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
newLocalUpdateBody); newLocalUpdateBody);
will(returnValue(newLocalUpdate)); will(returnValue(newLocalUpdate));
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate, oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
newLocalUpdateMeta, true); newLocalUpdateMeta, true, false);
// The client's visibility has changed // The client's visibility has changed
oneOf(clientHelper).getGroupMetadataAsDictionary(txn, oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
contactGroup.getId()); contactGroup.getId());

View File

@@ -34,7 +34,6 @@ import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent; import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.db.NoSuchContactException;
@@ -46,7 +45,6 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionRegistry; import org.briarproject.bramble.api.plugin.ConnectionRegistry;
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent; import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent; import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent; import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
import org.briarproject.bramble.api.sync.event.MessagesSentEvent; import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
@@ -65,10 +63,9 @@ import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.view.ImagePreview; import org.briarproject.briar.android.view.ImagePreview;
import org.briarproject.briar.android.view.TextAttachmentController; import org.briarproject.briar.android.view.TextAttachmentController;
import org.briarproject.briar.android.view.TextAttachmentController.AttachImageListener; import org.briarproject.briar.android.view.TextAttachmentController.AttachmentListener;
import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.BlogSharingManager; import org.briarproject.briar.api.blog.BlogSharingManager;
import org.briarproject.briar.api.client.ProtocolStateException; import org.briarproject.briar.api.client.ProtocolStateException;
@@ -83,7 +80,6 @@ import org.briarproject.briar.api.introduction.IntroductionManager;
import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessageFactory;
import org.briarproject.briar.api.messaging.PrivateMessageHeader; import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
@@ -94,7 +90,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
@@ -129,6 +124,7 @@ import static org.briarproject.briar.android.conversation.ImageActivity.NAME;
import static org.briarproject.briar.android.util.UiUtils.getAvatarTransitionName; import static org.briarproject.briar.android.util.UiUtils.getAvatarTransitionName;
import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName; import static org.briarproject.briar.android.util.UiUtils.getBulbTransitionName;
import static org.briarproject.briar.android.util.UiUtils.observeOnce; import static org.briarproject.briar.android.util.UiUtils.observeOnce;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH; import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH;
import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_DISMISSED; import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_DISMISSED;
import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_FINISHED; import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_FINISHED;
@@ -136,8 +132,8 @@ import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.S
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class ConversationActivity extends BriarActivity public class ConversationActivity extends BriarActivity
implements EventListener, ConversationListener, SendListener, implements EventListener, ConversationListener, TextCache,
TextCache, AttachmentCache, AttachImageListener { AttachmentCache, AttachmentListener {
public static final String CONTACT_ID = "briar.CONTACT_ID"; public static final String CONTACT_ID = "briar.CONTACT_ID";
@@ -152,9 +148,6 @@ public class ConversationActivity extends BriarActivity
@Inject @Inject
ConnectionRegistry connectionRegistry; ConnectionRegistry connectionRegistry;
@Inject @Inject
@CryptoExecutor
Executor cryptoExecutor;
@Inject
ViewModelProvider.Factory viewModelFactory; ViewModelProvider.Factory viewModelFactory;
@Inject @Inject
FeatureFlags featureFlags; FeatureFlags featureFlags;
@@ -169,10 +162,6 @@ public class ConversationActivity extends BriarActivity
@Inject @Inject
volatile EventBus eventBus; volatile EventBus eventBus;
@Inject @Inject
volatile SettingsManager settingsManager;
@Inject
volatile PrivateMessageFactory privateMessageFactory;
@Inject
volatile IntroductionManager introductionManager; volatile IntroductionManager introductionManager;
@Inject @Inject
volatile ForumSharingManager forumSharingManager; volatile ForumSharingManager forumSharingManager;
@@ -267,10 +256,10 @@ public class ConversationActivity extends BriarActivity
if (featureFlags.shouldEnableImageAttachments()) { if (featureFlags.shouldEnableImageAttachments()) {
ImagePreview imagePreview = findViewById(R.id.imagePreview); ImagePreview imagePreview = findViewById(R.id.imagePreview);
sendController = new TextAttachmentController(textInputView, sendController = new TextAttachmentController(textInputView,
imagePreview, this, this, viewModel); imagePreview, this, viewModel);
observeOnce(viewModel.hasImageSupport(), this, hasSupport -> { observeOnce(viewModel.hasImageSupport(), this, hasSupport -> {
if (hasSupport != null && hasSupport) { if (hasSupport != null && hasSupport) {
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS // TODO: remove cast when removing feature flag
((TextAttachmentController) sendController) ((TextAttachmentController) sendController)
.setImagesSupported(); .setImagesSupported();
} }
@@ -305,7 +294,7 @@ public class ConversationActivity extends BriarActivity
Snackbar.LENGTH_SHORT) Snackbar.LENGTH_SHORT)
.show(); .show();
} else if (request == REQUEST_ATTACH_IMAGE && result == RESULT_OK) { } else if (request == REQUEST_ATTACH_IMAGE && result == RESULT_OK) {
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS // TODO: remove cast when removing feature flag
((TextAttachmentController) sendController).onImageReceived(data); ((TextAttachmentController) sendController).onImageReceived(data);
} }
} }
@@ -454,7 +443,7 @@ public class ConversationActivity extends BriarActivity
if (text == null) { if (text == null) {
LOG.info("Eagerly loading text for latest message"); LOG.info("Eagerly loading text for latest message");
text = messagingManager.getMessageText(id); text = messagingManager.getMessageText(id);
textCache.put(id, text); textCache.put(id, requireNonNull(text));
} }
} }
// If the message has a single image, load its size - for multiple // If the message has a single image, load its size - for multiple
@@ -478,8 +467,10 @@ public class ConversationActivity extends BriarActivity
adapter.incrementRevision(); adapter.incrementRevision();
textInputView.setReady(true); textInputView.setReady(true);
// start observing onboarding after enabling // start observing onboarding after enabling
viewModel.showImageOnboarding().observeEvent(this, if (featureFlags.shouldEnableImageAttachments()) {
this::showImageOnboarding); viewModel.showImageOnboarding().observeEvent(this,
this::showImageOnboarding);
}
List<ConversationItem> items = createItems(headers); List<ConversationItem> items = createItems(headers);
adapter.addAll(items); adapter.addAll(items);
list.showData(); list.showData();
@@ -515,7 +506,7 @@ public class ConversationActivity extends BriarActivity
long start = now(); long start = now();
String text = messagingManager.getMessageText(m); String text = messagingManager.getMessageText(m);
logDuration(LOG, "Loading text", start); logDuration(LOG, "Loading text", start);
displayMessageText(m, text); displayMessageText(m, requireNonNull(text));
} catch (DbException e) { } catch (DbException e) {
logException(LOG, WARNING, e); logException(LOG, WARNING, e);
} }
@@ -660,6 +651,14 @@ public class ConversationActivity extends BriarActivity
startActivityForResult(intent, REQUEST_ATTACH_IMAGE); startActivityForResult(intent, REQUEST_ATTACH_IMAGE);
} }
@Override
public void onTooManyAttachments() {
String format = getResources().getString(
R.string.messaging_too_many_attachments_toast);
String warning = String.format(format, MAX_ATTACHMENTS_PER_MESSAGE);
Toast.makeText(this, warning, LENGTH_SHORT).show();
}
@Override @Override
public void onSendClick(@Nullable String text, public void onSendClick(@Nullable String text,
List<AttachmentHeader> attachmentHeaders) { List<AttachmentHeader> attachmentHeaders) {
@@ -729,7 +728,7 @@ public class ConversationActivity extends BriarActivity
} }
private void showImageOnboarding() { private void showImageOnboarding() {
// remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS // TODO: remove cast when removing feature flag
((TextAttachmentController) sendController) ((TextAttachmentController) sendController)
.showImageOnboarding(this, () -> .showImageOnboarding(this, () ->
viewModel.onImageOnboardingSeen()); viewModel.onImageOnboardingSeen());

View File

@@ -61,6 +61,7 @@ public class ConversationViewModel extends AndroidViewModel
private static Logger LOG = private static Logger LOG =
getLogger(ConversationViewModel.class.getName()); getLogger(ConversationViewModel.class.getName());
private static final String SHOW_ONBOARDING_IMAGE = private static final String SHOW_ONBOARDING_IMAGE =
"showOnboardingImage"; "showOnboardingImage";
private static final String SHOW_ONBOARDING_INTRODUCTION = private static final String SHOW_ONBOARDING_INTRODUCTION =
@@ -181,12 +182,17 @@ public class ConversationViewModel extends AndroidViewModel
}); });
} }
@UiThread
void sendMessage(@Nullable String text, void sendMessage(@Nullable String text,
List<AttachmentHeader> attachmentHeaders, long timestamp) { List<AttachmentHeader> headers, long timestamp) {
// messagingGroupId is loaded with the contact // messagingGroupId is loaded with the contact
observeForeverOnce(messagingGroupId, groupId -> { observeForeverOnce(messagingGroupId, groupId -> {
if (groupId == null) throw new IllegalStateException(); requireNonNull(groupId);
createMessage(groupId, text, attachmentHeaders, timestamp); observeForeverOnce(imageSupport, hasImageSupport -> {
requireNonNull(hasImageSupport);
createMessage(groupId, text, headers, timestamp,
hasImageSupport);
});
}); });
} }
@@ -270,21 +276,24 @@ public class ConversationViewModel extends AndroidViewModel
} }
private void createMessage(GroupId groupId, @Nullable String text, private void createMessage(GroupId groupId, @Nullable String text,
List<AttachmentHeader> attachments, long timestamp) { List<AttachmentHeader> headers, long timestamp,
boolean hasImageSupport) {
try { try {
// TODO remove when text can be null in the backend PrivateMessage pm;
String msgText = text == null ? "null" : text; if (hasImageSupport) {
PrivateMessage pm = privateMessageFactory pm = privateMessageFactory.createPrivateMessage(groupId,
.createPrivateMessage(groupId, timestamp, msgText, timestamp, text, headers);
attachments); } else {
storeMessage(pm, msgText, attachments); pm = privateMessageFactory.createLegacyPrivateMessage(
groupId, timestamp, requireNonNull(text));
}
storeMessage(pm);
} catch (FormatException e) { } catch (FormatException e) {
throw new RuntimeException(e); throw new AssertionError(e);
} }
} }
private void storeMessage(PrivateMessage m, @Nullable String text, private void storeMessage(PrivateMessage m) {
List<AttachmentHeader> attachments) {
attachmentCreator.onAttachmentsSent(m.getMessage().getId()); attachmentCreator.onAttachmentsSent(m.getMessage().getId());
dbExecutor.execute(() -> { dbExecutor.execute(() -> {
try { try {
@@ -295,7 +304,7 @@ public class ConversationViewModel extends AndroidViewModel
PrivateMessageHeader h = new PrivateMessageHeader( PrivateMessageHeader h = new PrivateMessageHeader(
message.getId(), message.getGroupId(), message.getId(), message.getGroupId(),
message.getTimestamp(), true, true, false, false, message.getTimestamp(), true, true, false, false,
text != null, attachments); m.hasText(), m.getAttachmentHeaders());
// TODO add text to cache when available here // TODO add text to cache when available here
addedHeader.postEvent(h); addedHeader.postEvent(h);
} catch (DbException e) { } catch (DbException e) {

View File

@@ -41,9 +41,9 @@ import static android.support.v4.content.ContextCompat.getColor;
import static android.support.v4.view.AbsSavedState.EMPTY_STATE; import static android.support.v4.view.AbsSavedState.EMPTY_STATE;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.widget.Toast.LENGTH_LONG; import static android.widget.Toast.LENGTH_LONG;
import static java.util.Objects.requireNonNull;
import static org.briarproject.briar.android.util.UiUtils.resolveColorAttribute; import static org.briarproject.briar.android.util.UiUtils.resolveColorAttribute;
import static org.briarproject.briar.api.messaging.MessagingConstants.IMAGE_MIME_TYPES; import static org.briarproject.briar.api.messaging.MessagingConstants.IMAGE_MIME_TYPES;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_DISMISSED; import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_DISMISSED;
import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_FINISHED; import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_FINISHED;
@@ -53,7 +53,7 @@ public class TextAttachmentController extends TextSendController
implements ImagePreviewListener { implements ImagePreviewListener {
private final ImagePreview imagePreview; private final ImagePreview imagePreview;
private final AttachImageListener imageListener; private final AttachmentListener attachmentListener;
private final CompositeSendButton sendButton; private final CompositeSendButton sendButton;
private final AttachmentManager attachmentManager; private final AttachmentManager attachmentManager;
@@ -62,10 +62,10 @@ public class TextAttachmentController extends TextSendController
private boolean loadingUris = false; private boolean loadingUris = false;
public TextAttachmentController(TextInputView v, ImagePreview imagePreview, public TextAttachmentController(TextInputView v, ImagePreview imagePreview,
SendListener listener, AttachImageListener imageListener, AttachmentListener attachmentListener,
AttachmentManager attachmentManager) { AttachmentManager attachmentManager) {
super(v, listener, false); super(v, attachmentListener, false);
this.imageListener = imageListener; this.attachmentListener = attachmentListener;
this.imagePreview = imagePreview; this.imagePreview = imagePreview;
this.attachmentManager = attachmentManager; this.attachmentManager = attachmentManager;
this.imagePreview.setImagePreviewListener(this); this.imagePreview.setImagePreviewListener(this);
@@ -124,8 +124,8 @@ public class TextAttachmentController extends TextSendController
return; return;
} }
Intent intent = getAttachFileIntent(); Intent intent = getAttachFileIntent();
if (imageListener.getLifecycle().getCurrentState() != DESTROYED) { if (attachmentListener.getLifecycle().getCurrentState() != DESTROYED) {
requireNonNull(imageListener).onAttachImage(intent); attachmentListener.onAttachImage(intent);
} }
} }
@@ -144,30 +144,38 @@ public class TextAttachmentController extends TextSendController
* returned by the Activity started with {@link #getAttachFileIntent()}. * returned by the Activity started with {@link #getAttachFileIntent()}.
* <p> * <p>
* This method must be called at most once per call to * This method must be called at most once per call to
* {@link AttachImageListener#onAttachImage(Intent)}. * {@link AttachmentListener#onAttachImage(Intent)}.
* Normally, this is true if called from * Normally, this is true if called from
* {@link Activity#onActivityResult(int, int, Intent)} since this is called * {@link Activity#onActivityResult(int, int, Intent)} since this is called
* at most once per call to {@link Activity#startActivityForResult(Intent, int)}. * at most once per call to
* {@link Activity#startActivityForResult(Intent, int)}.
*/ */
@SuppressWarnings("JavadocReference")
public void onImageReceived(@Nullable Intent resultData) { public void onImageReceived(@Nullable Intent resultData) {
if (resultData == null) return; if (resultData == null) return;
if (loadingUris || !imageUris.isEmpty()) throw new AssertionError(); if (loadingUris || !imageUris.isEmpty()) throw new AssertionError();
List<Uri> newUris = new ArrayList<>();
if (resultData.getData() != null) { if (resultData.getData() != null) {
imageUris.add(resultData.getData()); newUris.add(resultData.getData());
onNewUris(false); onNewUris(false, newUris);
} else if (SDK_INT >= 18 && resultData.getClipData() != null) { } else if (SDK_INT >= 18 && resultData.getClipData() != null) {
ClipData clipData = resultData.getClipData(); ClipData clipData = resultData.getClipData();
for (int i = 0; i < clipData.getItemCount(); i++) { for (int i = 0; i < clipData.getItemCount(); i++) {
imageUris.add(clipData.getItemAt(i).getUri()); newUris.add(clipData.getItemAt(i).getUri());
} }
onNewUris(false); onNewUris(false, newUris);
} }
} }
private void onNewUris(boolean restart) { private void onNewUris(boolean restart, List<Uri> newUris) {
if (imageUris.isEmpty()) return; if (newUris.isEmpty()) return;
if (loadingUris) throw new AssertionError(); if (loadingUris) throw new AssertionError();
loadingUris = true; loadingUris = true;
if (newUris.size() > MAX_ATTACHMENTS_PER_MESSAGE) {
newUris = newUris.subList(0, MAX_ATTACHMENTS_PER_MESSAGE);
attachmentListener.onTooManyAttachments();
}
imageUris.addAll(newUris);
updateViewState(); updateViewState();
textInput.setHint(R.string.image_caption_hint); textInput.setHint(R.string.image_caption_hint);
List<ImagePreviewItem> items = ImagePreviewItem.fromUris(imageUris); List<ImagePreviewItem> items = ImagePreviewItem.fromUris(imageUris);
@@ -175,7 +183,7 @@ public class TextAttachmentController extends TextSendController
// store attachments and show preview when successful // store attachments and show preview when successful
LiveData<AttachmentResult> result = LiveData<AttachmentResult> result =
attachmentManager.storeAttachments(imageUris, restart); attachmentManager.storeAttachments(imageUris, restart);
result.observe(imageListener, new Observer<AttachmentResult>() { result.observe(attachmentListener, new Observer<AttachmentResult>() {
@Override @Override
public void onChanged(@Nullable AttachmentResult attachmentResult) { public void onChanged(@Nullable AttachmentResult attachmentResult) {
if (attachmentResult == null) { if (attachmentResult == null) {
@@ -240,8 +248,7 @@ public class TextAttachmentController extends TextSendController
public Parcelable onRestoreInstanceState(Parcelable inState) { public Parcelable onRestoreInstanceState(Parcelable inState) {
SavedState state = (SavedState) inState; SavedState state = (SavedState) inState;
if (!imageUris.isEmpty()) throw new AssertionError(); if (!imageUris.isEmpty()) throw new AssertionError();
if (state.imageUris != null) imageUris.addAll(state.imageUris); if (state.imageUris != null) onNewUris(true, state.imageUris);
onNewUris(true);
return state.getSuperState(); return state.getSuperState();
} }
@@ -316,8 +323,11 @@ public class TextAttachmentController extends TextSendController
}; };
} }
public interface AttachImageListener extends LifecycleOwner { @UiThread
void onAttachImage(Intent intent); public interface AttachmentListener extends SendListener, LifecycleOwner {
}
void onAttachImage(Intent intent);
void onTooManyAttachments();
}
} }

View File

@@ -84,6 +84,7 @@ public class TextSendController implements TextInputListener {
return state; return state;
} }
@UiThread
public interface SendListener { public interface SendListener {
void onSendClick(@Nullable String text, List<AttachmentHeader> headers); void onSendClick(@Nullable String text, List<AttachmentHeader> headers);
} }

View File

@@ -150,6 +150,7 @@
<string name="dialog_message_no_image_support">Your contact\'s Briar does not yet support image attachments. Once they upgrade you\'ll see a different icon.</string> <string name="dialog_message_no_image_support">Your contact\'s Briar does not yet support image attachments. Once they upgrade you\'ll see a different icon.</string>
<string name="dialog_title_image_support">You can now send images to this contact</string> <string name="dialog_title_image_support">You can now send images to this contact</string>
<string name="dialog_message_image_support">Tap this icon to attach images.</string> <string name="dialog_message_image_support">Tap this icon to attach images.</string>
<string name="messaging_too_many_attachments_toast">Only the first %d images will be sent</string>
<!-- Adding Contacts --> <!-- Adding Contacts -->

View File

@@ -4,26 +4,30 @@ import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.messaging.PrivateMessage;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
public abstract class ThreadedMessage extends PrivateMessage { public abstract class ThreadedMessage {
private final Message message;
@Nullable @Nullable
private final MessageId parent; private final MessageId parent;
private final Author author; private final Author author;
public ThreadedMessage(Message message, @Nullable MessageId parent, public ThreadedMessage(Message message, @Nullable MessageId parent,
Author author) { Author author) {
super(message); this.message = message;
this.parent = parent; this.parent = parent;
this.author = author; this.author = author;
} }
public Message getMessage() {
return message;
}
@Nullable @Nullable
public MessageId getParent() { public MessageId getParent() {
return parent; return parent;

View File

@@ -18,7 +18,8 @@ public abstract class ConversationMessageReceivedEvent<H extends ConversationMes
private final H messageHeader; private final H messageHeader;
private final ContactId contactId; private final ContactId contactId;
public ConversationMessageReceivedEvent(H messageHeader, ContactId contactId) { public ConversationMessageReceivedEvent(H messageHeader,
ContactId contactId) {
this.messageHeader = messageHeader; this.messageHeader = messageHeader;
this.contactId = contactId; this.contactId = contactId;
} }

View File

@@ -9,6 +9,16 @@ public interface MessagingConstants {
*/ */
int MAX_PRIVATE_MESSAGE_TEXT_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024; int MAX_PRIVATE_MESSAGE_TEXT_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024;
/**
* The maximum number of attachments per private message.
*/
int MAX_ATTACHMENTS_PER_MESSAGE = 10;
/**
* The maximum length of an attachment's content type in UTF-8 bytes.
*/
int MAX_CONTENT_TYPE_BYTES = 50;
/** /**
* The supported mime types for image attachments. * The supported mime types for image attachments.
*/ */
@@ -22,6 +32,6 @@ public interface MessagingConstants {
* The maximum allowed size of image attachments. * The maximum allowed size of image attachments.
* TODO: Different limit for GIFs? * TODO: Different limit for GIFs?
*/ */
int MAX_IMAGE_SIZE = MAX_MESSAGE_BODY_LENGTH; // 6 * 1024 * 1024; int MAX_IMAGE_SIZE = MAX_MESSAGE_BODY_LENGTH - 100; // 6 * 1024 * 1024;
} }

View File

@@ -12,6 +12,8 @@ import org.briarproject.briar.api.conversation.ConversationManager.ConversationC
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import javax.annotation.Nullable;
@NotNullByDefault @NotNullByDefault
public interface MessagingManager extends ConversationClient { public interface MessagingManager extends ConversationClient {
@@ -28,7 +30,7 @@ public interface MessagingManager extends ConversationClient {
/** /**
* The current minor version of the messaging client. * The current minor version of the messaging client.
*/ */
int MINOR_VERSION = 0; int MINOR_VERSION = 1;
/** /**
* Stores a local private message. * Stores a local private message.
@@ -59,8 +61,10 @@ public interface MessagingManager extends ConversationClient {
GroupId getConversationId(ContactId c) throws DbException; GroupId getConversationId(ContactId c) throws DbException;
/** /**
* Returns the text of the private message with the given ID. * Returns the text of the private message with the given ID, or null if
* the private message has no text.
*/ */
@Nullable
String getMessageText(MessageId m) throws DbException; String getMessageText(MessageId m) throws DbException;
/** /**

View File

@@ -3,20 +3,56 @@ package org.briarproject.briar.api.messaging;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import java.util.List;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import static java.util.Collections.emptyList;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
public class PrivateMessage { public class PrivateMessage {
private final Message message; private final Message message;
private final boolean legacyFormat, hasText;
private final List<AttachmentHeader> attachmentHeaders;
/**
* Constructor for private messages in the legacy format, which does not
* support attachments.
*/
public PrivateMessage(Message message) { public PrivateMessage(Message message) {
this.message = message; this.message = message;
legacyFormat = true;
hasText = true;
attachmentHeaders = emptyList();
}
/**
* Constructor for private messages in the current format, which supports
* attachments.
*/
public PrivateMessage(Message message, boolean hasText,
List<AttachmentHeader> headers) {
this.message = message;
this.hasText = hasText;
this.attachmentHeaders = headers;
legacyFormat = false;
} }
public Message getMessage() { public Message getMessage() {
return message; return message;
} }
public boolean isLegacyFormat() {
return legacyFormat;
}
public boolean hasText() {
return hasText;
}
public List<AttachmentHeader> getAttachmentHeaders() {
return attachmentHeaders;
}
} }

View File

@@ -6,11 +6,16 @@ import org.briarproject.bramble.api.sync.GroupId;
import java.util.List; import java.util.List;
import javax.annotation.Nullable;
@NotNullByDefault @NotNullByDefault
public interface PrivateMessageFactory { public interface PrivateMessageFactory {
PrivateMessage createLegacyPrivateMessage(GroupId groupId, long timestamp,
String text) throws FormatException;
PrivateMessage createPrivateMessage(GroupId groupId, long timestamp, PrivateMessage createPrivateMessage(GroupId groupId, long timestamp,
String text, List<AttachmentHeader> attachments) @Nullable String text, List<AttachmentHeader> headers)
throws FormatException; throws FormatException;
} }

View File

@@ -19,10 +19,10 @@ public class PrivateMessageHeader extends ConversationMessageHeader {
public PrivateMessageHeader(MessageId id, GroupId groupId, long timestamp, public PrivateMessageHeader(MessageId id, GroupId groupId, long timestamp,
boolean local, boolean read, boolean sent, boolean seen, boolean local, boolean read, boolean sent, boolean seen,
boolean hasText, List<AttachmentHeader> attachmentHeaders) { boolean hasText, List<AttachmentHeader> headers) {
super(id, groupId, timestamp, local, read, sent, seen); super(id, groupId, timestamp, local, read, sent, seen);
this.hasText = hasText; this.hasText = hasText;
this.attachmentHeaders = attachmentHeaders; this.attachmentHeaders = headers;
} }
public boolean hasText() { public boolean hasText() {

View File

@@ -0,0 +1,32 @@
package org.briarproject.briar.api.messaging.event;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId;
import javax.annotation.concurrent.Immutable;
/**
* An event that is broadcast when a new attachment is received.
*/
@Immutable
@NotNullByDefault
public class AttachmentReceivedEvent extends Event {
private final MessageId messageId;
private final ContactId contactId;
public AttachmentReceivedEvent(MessageId messageId, ContactId contactId) {
this.messageId = messageId;
this.contactId = contactId;
}
public MessageId getMessageId() {
return messageId;
}
public ContactId getContactId() {
return contactId;
}
}

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.api.test; package org.briarproject.briar.api.test;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.IoExecutor;
@@ -24,9 +23,4 @@ public interface TestDataCreator {
@IoExecutor @IoExecutor
Contact addContact(String name) throws DbException; Contact addContact(String name) throws DbException;
@IoExecutor
void addPrivateMessage(Contact contact, String text, long time,
boolean local) throws DbException, FormatException;
} }

View File

@@ -234,7 +234,8 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
meta.put(KEY_AUTHOR, clientHelper.toList(p.getAuthor())); meta.put(KEY_AUTHOR, clientHelper.toList(p.getAuthor()));
meta.put(KEY_READ, true); meta.put(KEY_READ, true);
meta.put(KEY_RSS_FEED, b.isRssFeed()); meta.put(KEY_RSS_FEED, b.isRssFeed());
clientHelper.addLocalMessage(txn, p.getMessage(), meta, true); clientHelper.addLocalMessage(txn, p.getMessage(), meta, true,
false);
// broadcast event about new post // broadcast event about new post
MessageId postId = p.getMessage().getId(); MessageId postId = p.getMessage().getId();
@@ -279,7 +280,7 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
meta.put(KEY_AUTHOR, clientHelper.toList(author)); meta.put(KEY_AUTHOR, clientHelper.toList(author));
// Send comment // Send comment
clientHelper.addLocalMessage(txn, message, meta, true); clientHelper.addLocalMessage(txn, message, meta, true, false);
// broadcast event // broadcast event
BlogPostHeader h = getPostHeaderFromMetadata(txn, groupId, BlogPostHeader h = getPostHeaderFromMetadata(txn, groupId,
@@ -377,7 +378,7 @@ class BlogManagerImpl extends BdfIncomingMessageHook implements BlogManager,
meta.put(KEY_TIME_RECEIVED, header.getTimeReceived()); meta.put(KEY_TIME_RECEIVED, header.getTimeReceived());
// Send wrapped message and store metadata // Send wrapped message and store metadata
clientHelper.addLocalMessage(txn, wrappedMessage, meta, true); clientHelper.addLocalMessage(txn, wrappedMessage, meta, true, false);
return wrappedMessage.getId(); return wrappedMessage.getId();
} }

View File

@@ -136,7 +136,8 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
meta.put(KEY_AUTHOR, clientHelper.toList(a)); meta.put(KEY_AUTHOR, clientHelper.toList(a));
meta.put(KEY_LOCAL, true); meta.put(KEY_LOCAL, true);
meta.put(MSG_KEY_READ, true); meta.put(MSG_KEY_READ, true);
clientHelper.addLocalMessage(txn, p.getMessage(), meta, true); clientHelper.addLocalMessage(txn, p.getMessage(), meta, true,
false);
messageTracker.trackOutgoingMessage(txn, p.getMessage()); messageTracker.trackOutgoingMessage(txn, p.getMessage());
} catch (FormatException e) { } catch (FormatException e) {
throw new AssertionError(e); throw new AssertionError(e);

View File

@@ -140,7 +140,7 @@ abstract class AbstractProtocolEngine<S extends Session>
.encodeMetadata(type, sessionId, m.getTimestamp(), true, true, .encodeMetadata(type, sessionId, m.getTimestamp(), true, true,
visibleInConversation); visibleInConversation);
try { try {
clientHelper.addLocalMessage(txn, m, meta, true); clientHelper.addLocalMessage(txn, m, meta, true, false);
} catch (FormatException e) { } catch (FormatException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
@@ -177,8 +177,7 @@ abstract class AbstractProtocolEngine<S extends Session>
boolean isInvalidDependency(@Nullable MessageId lastRemoteMessageId, boolean isInvalidDependency(@Nullable MessageId lastRemoteMessageId,
@Nullable MessageId dependency) { @Nullable MessageId dependency) {
if (dependency == null) return lastRemoteMessageId != null; if (dependency == null) return lastRemoteMessageId != null;
return lastRemoteMessageId == null || return !dependency.equals(lastRemoteMessageId);
!dependency.equals(lastRemoteMessageId);
} }
long getLocalTimestamp(long localTimestamp, long requestTimestamp) { long getLocalTimestamp(long localTimestamp, long requestTimestamp) {

View File

@@ -0,0 +1,60 @@
package org.briarproject.briar.messaging;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.concurrent.NotThreadSafe;
/**
* An {@link InputStream} that wraps another InputStream, counting the bytes
* read and only allowing a limited number of bytes to be read.
*/
@NotThreadSafe
@NotNullByDefault
class CountingInputStream extends InputStream {
private final InputStream delegate;
private final long maxBytesToRead;
private long bytesRead = 0;
CountingInputStream(InputStream delegate, long maxBytesToRead) {
this.delegate = delegate;
this.maxBytesToRead = maxBytesToRead;
}
long getBytesRead() {
return bytesRead;
}
@Override
public int available() throws IOException {
return delegate.available();
}
@Override
public void close() throws IOException {
delegate.close();
}
@Override
public int read() throws IOException {
if (bytesRead == maxBytesToRead) return -1;
int i = delegate.read();
if (i != -1) bytesRead++;
return i;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (len == 0) return 0;
if (bytesRead == maxBytesToRead) return -1;
if (bytesRead + len > maxBytesToRead)
len = (int) (maxBytesToRead - bytesRead);
int read = delegate.read(b, off, len);
if (read != -1) bytesRead += read;
return read;
}
}

View File

@@ -0,0 +1,7 @@
package org.briarproject.briar.messaging;
interface MessageTypes {
int PRIVATE_MESSAGE = 0;
int ATTACHMENT = 1;
}

View File

@@ -0,0 +1,16 @@
package org.briarproject.briar.messaging;
interface MessagingConstants {
// Metadata keys for groups
String GROUP_KEY_CONTACT_ID = "contactId";
// Metadata keys for messages
String MSG_KEY_TIMESTAMP = "timestamp";
String MSG_KEY_LOCAL = "local";
String MSG_KEY_MSG_TYPE = "messageType";
String MSG_KEY_CONTENT_TYPE = "contentType";
String MSG_KEY_DESCRIPTOR_LENGTH = "descriptorLength";
String MSG_KEY_HAS_TEXT = "hasText";
String MSG_KEY_ATTACHMENT_HEADERS = "attachmentHeaders";
}

View File

@@ -11,20 +11,23 @@ import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.data.MetadataParser; import org.briarproject.bramble.api.data.MetadataParser;
import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.Metadata;
import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.lifecycle.LifecycleManager.OpenDatabaseHook; import org.briarproject.bramble.api.lifecycle.LifecycleManager.OpenDatabaseHook;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Group; import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.sync.Group.Visibility; import org.briarproject.bramble.api.sync.Group.Visibility;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.InvalidMessageException;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageFactory;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.MessageStatus; import org.briarproject.bramble.api.sync.MessageStatus;
import org.briarproject.bramble.api.sync.validation.IncomingMessageHook;
import org.briarproject.bramble.api.versioning.ClientVersioningManager; import org.briarproject.bramble.api.versioning.ClientVersioningManager;
import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook; import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook;
import org.briarproject.bramble.util.IoUtils;
import org.briarproject.briar.api.client.MessageTracker; import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
import org.briarproject.briar.api.conversation.ConversationManager.ConversationClient;
import org.briarproject.briar.api.conversation.ConversationMessageHeader; import org.briarproject.briar.api.conversation.ConversationMessageHeader;
import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
@@ -32,15 +35,16 @@ import org.briarproject.briar.api.messaging.FileTooBigException;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessage; import org.briarproject.briar.api.messaging.PrivateMessage;
import org.briarproject.briar.api.messaging.PrivateMessageHeader; import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent; import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent;
import org.briarproject.briar.client.ConversationClientImpl;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.EOFException; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.Map; import java.util.Map;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
@@ -48,28 +52,57 @@ import javax.inject.Inject;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_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.briar.client.MessageTrackerConstants.MSG_KEY_READ; import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ;
import static org.briarproject.briar.messaging.MessageTypes.ATTACHMENT;
import static org.briarproject.briar.messaging.MessageTypes.PRIVATE_MESSAGE;
import static org.briarproject.briar.messaging.MessagingConstants.GROUP_KEY_CONTACT_ID;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_ATTACHMENT_HEADERS;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_CONTENT_TYPE;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_DESCRIPTOR_LENGTH;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_HAS_TEXT;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_LOCAL;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_MSG_TYPE;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_TIMESTAMP;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
class MessagingManagerImpl extends ConversationClientImpl class MessagingManagerImpl implements MessagingManager, IncomingMessageHook,
implements MessagingManager, OpenDatabaseHook, ContactHook, ConversationClient, OpenDatabaseHook, ContactHook,
ClientVersioningHook { ClientVersioningHook {
private final DatabaseComponent db;
private final ClientHelper clientHelper;
private final MetadataParser metadataParser;
private final MessageTracker messageTracker;
private final ClientVersioningManager clientVersioningManager; private final ClientVersioningManager clientVersioningManager;
private final ContactGroupFactory contactGroupFactory; private final ContactGroupFactory contactGroupFactory;
private final MessageFactory messageFactory;
@Inject @Inject
MessagingManagerImpl(DatabaseComponent db, ClientHelper clientHelper, MessagingManagerImpl(DatabaseComponent db, ClientHelper clientHelper,
ClientVersioningManager clientVersioningManager, ClientVersioningManager clientVersioningManager,
MetadataParser metadataParser, MessageTracker messageTracker, MetadataParser metadataParser, MessageTracker messageTracker,
ContactGroupFactory contactGroupFactory, ContactGroupFactory contactGroupFactory) {
MessageFactory messageFactory) { this.db = db;
super(db, clientHelper, metadataParser, messageTracker); this.clientHelper = clientHelper;
this.metadataParser = metadataParser;
this.messageTracker = messageTracker;
this.clientVersioningManager = clientVersioningManager; this.clientVersioningManager = clientVersioningManager;
this.contactGroupFactory = contactGroupFactory; this.contactGroupFactory = contactGroupFactory;
this.messageFactory = messageFactory; }
@Override
public GroupCount getGroupCount(Transaction txn, ContactId contactId)
throws DbException {
Contact contact = db.getContact(txn, contactId);
GroupId groupId = getContactGroup(contact).getId();
return messageTracker.getGroupCount(txn, groupId);
}
@Override
public void setReadFlag(GroupId g, MessageId m, boolean read)
throws DbException {
messageTracker.setReadFlag(g, m, read);
} }
@Override @Override
@@ -94,14 +127,14 @@ class MessagingManagerImpl extends ConversationClientImpl
db.setGroupVisibility(txn, c.getId(), g.getId(), client); db.setGroupVisibility(txn, c.getId(), g.getId(), client);
// Attach the contact ID to the group // Attach the contact ID to the group
BdfDictionary d = new BdfDictionary(); BdfDictionary d = new BdfDictionary();
d.put("contactId", c.getId().getInt()); d.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
try { try {
clientHelper.mergeGroupMetadata(txn, g.getId(), d); clientHelper.mergeGroupMetadata(txn, g.getId(), d);
} catch (FormatException e) { } catch (FormatException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
// Initialize the group count with current time // Initialize the group count with current time
initializeGroupCount(txn, g.getId()); messageTracker.initializeGroupCount(txn, g.getId());
} }
@Override @Override
@@ -124,24 +157,66 @@ class MessagingManagerImpl extends ConversationClientImpl
} }
@Override @Override
protected boolean incomingMessage(Transaction txn, Message m, BdfList body, public boolean incomingMessage(Transaction txn, Message m, Metadata meta)
BdfDictionary meta) throws DbException, FormatException { throws DbException, InvalidMessageException {
try {
BdfDictionary metaDict = metadataParser.parse(meta);
// Message type is null for version 0.0 private messages
Long messageType = metaDict.getOptionalLong(MSG_KEY_MSG_TYPE);
if (messageType == null) {
incomingPrivateMessage(txn, m, metaDict, true, emptyList());
} else if (messageType == PRIVATE_MESSAGE) {
boolean hasText = metaDict.getBoolean(MSG_KEY_HAS_TEXT);
List<AttachmentHeader> headers =
parseAttachmentHeaders(metaDict);
incomingPrivateMessage(txn, m, metaDict, hasText, headers);
} else if (messageType == ATTACHMENT) {
incomingAttachment(txn, m);
} else {
throw new InvalidMessageException();
}
} catch (FormatException e) {
throw new InvalidMessageException(e);
}
// Don't share message
return false;
}
private void incomingPrivateMessage(Transaction txn, Message m,
BdfDictionary meta, boolean hasText, List<AttachmentHeader> headers)
throws DbException, FormatException {
GroupId groupId = m.getGroupId(); GroupId groupId = m.getGroupId();
long timestamp = meta.getLong("timestamp"); long timestamp = meta.getLong(MSG_KEY_TIMESTAMP);
boolean local = meta.getBoolean("local"); boolean local = meta.getBoolean(MSG_KEY_LOCAL);
boolean read = meta.getBoolean(MSG_KEY_READ); boolean read = meta.getBoolean(MSG_KEY_READ);
PrivateMessageHeader header = PrivateMessageHeader header =
new PrivateMessageHeader(m.getId(), groupId, timestamp, local, new PrivateMessageHeader(m.getId(), groupId, timestamp, local,
read, false, false, true, emptyList()); read, false, false, hasText, headers);
ContactId contactId = getContactId(txn, groupId); ContactId contactId = getContactId(txn, groupId);
PrivateMessageReceivedEvent event = PrivateMessageReceivedEvent event =
new PrivateMessageReceivedEvent(header, contactId); new PrivateMessageReceivedEvent(header, contactId);
txn.attach(event); txn.attach(event);
messageTracker.trackIncomingMessage(txn, m); messageTracker.trackIncomingMessage(txn, m);
}
// don't share message private List<AttachmentHeader> parseAttachmentHeaders(BdfDictionary meta)
return false; throws FormatException {
BdfList attachmentHeaders = meta.getList(MSG_KEY_ATTACHMENT_HEADERS);
int length = attachmentHeaders.size();
List<AttachmentHeader> headers = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
BdfList header = attachmentHeaders.getList(i);
MessageId id = new MessageId(header.getRaw(0));
String contentType = header.getString(1);
headers.add(new AttachmentHeader(id, contentType));
}
return headers;
}
private void incomingAttachment(Transaction txn, Message m)
throws DbException {
ContactId contactId = getContactId(txn, m.getGroupId());
txn.attach(new AttachmentReceivedEvent(m.getId(), contactId));
} }
@Override @Override
@@ -149,14 +224,30 @@ class MessagingManagerImpl extends ConversationClientImpl
Transaction txn = db.startTransaction(false); Transaction txn = db.startTransaction(false);
try { try {
BdfDictionary meta = new BdfDictionary(); BdfDictionary meta = new BdfDictionary();
meta.put("timestamp", m.getMessage().getTimestamp()); meta.put(MSG_KEY_TIMESTAMP, m.getMessage().getTimestamp());
meta.put("local", true); meta.put(MSG_KEY_LOCAL, true);
meta.put("read", true); meta.put(MSG_KEY_READ, true);
clientHelper.addLocalMessage(txn, m.getMessage(), meta, true); if (!m.isLegacyFormat()) {
meta.put(MSG_KEY_MSG_TYPE, PRIVATE_MESSAGE);
meta.put(MSG_KEY_HAS_TEXT, m.hasText());
BdfList headers = new BdfList();
for (AttachmentHeader a : m.getAttachmentHeaders()) {
headers.add(
BdfList.of(a.getMessageId(), a.getContentType()));
}
meta.put(MSG_KEY_ATTACHMENT_HEADERS, headers);
}
// Mark attachments as shared and permanent now we're ready to send
for (AttachmentHeader a : m.getAttachmentHeaders()) {
db.setMessageShared(txn, a.getMessageId());
db.setMessagePermanent(txn, a.getMessageId());
}
clientHelper.addLocalMessage(txn, m.getMessage(), meta, true,
false);
messageTracker.trackOutgoingMessage(txn, m.getMessage()); messageTracker.trackOutgoingMessage(txn, m.getMessage());
db.commitTransaction(txn); db.commitTransaction(txn);
} catch (FormatException e) { } catch (FormatException e) {
throw new RuntimeException(e); throw new AssertionError(e);
} finally { } finally {
db.endTransaction(txn); db.endTransaction(txn);
} }
@@ -164,22 +255,27 @@ class MessagingManagerImpl extends ConversationClientImpl
@Override @Override
public AttachmentHeader addLocalAttachment(GroupId groupId, long timestamp, public AttachmentHeader addLocalAttachment(GroupId groupId, long timestamp,
String contentType, InputStream is) String contentType, InputStream in)
throws DbException, IOException { throws DbException, IOException {
// TODO add real implementation // TODO: Support large messages
byte[] body = new byte[MAX_MESSAGE_BODY_LENGTH]; ByteArrayOutputStream bodyOut = new ByteArrayOutputStream();
try { byte[] descriptor =
IoUtils.read(is, body); clientHelper.toByteArray(BdfList.of(ATTACHMENT, contentType));
} catch (EOFException ignored) { bodyOut.write(descriptor);
} copyAndClose(in, bodyOut);
if (is.available() > 0) throw new FileTooBigException(); if (bodyOut.size() > MAX_MESSAGE_BODY_LENGTH)
is.close(); throw new FileTooBigException();
try { byte[] body = bodyOut.toByteArray();
Thread.sleep(1000); BdfDictionary meta = new BdfDictionary();
} catch (InterruptedException ignored) { meta.put(MSG_KEY_TIMESTAMP, timestamp);
} meta.put(MSG_KEY_LOCAL, true);
Message m = messageFactory.createMessage(groupId, timestamp, body); meta.put(MSG_KEY_MSG_TYPE, ATTACHMENT);
clientHelper.addLocalMessage(m, new BdfDictionary(), false); meta.put(MSG_KEY_CONTENT_TYPE, contentType);
meta.put(MSG_KEY_DESCRIPTOR_LENGTH, descriptor.length);
Message m = clientHelper.createMessage(groupId, timestamp, body);
// Mark attachments as temporary, not shared until we're ready to send
db.transaction(false, txn ->
clientHelper.addLocalMessage(txn, m, meta, false, true));
return new AttachmentHeader(m.getId(), contentType); return new AttachmentHeader(m.getId(), contentType);
} }
@@ -242,11 +338,23 @@ class MessagingManagerImpl extends ConversationClientImpl
BdfDictionary meta = metadata.get(id); BdfDictionary meta = metadata.get(id);
if (meta == null) continue; if (meta == null) continue;
try { try {
long timestamp = meta.getLong("timestamp"); // Message type is null for version 0.0 private messages
boolean local = meta.getBoolean("local"); Long messageType = meta.getOptionalLong(MSG_KEY_MSG_TYPE);
boolean read = meta.getBoolean("read"); if (messageType != null && messageType != PRIVATE_MESSAGE)
headers.add(new PrivateMessageHeader(id, g, timestamp, local, continue;
read, s.isSent(), s.isSeen(), true, emptyList())); long timestamp = meta.getLong(MSG_KEY_TIMESTAMP);
boolean local = meta.getBoolean(MSG_KEY_LOCAL);
boolean read = meta.getBoolean(MSG_KEY_READ);
if (messageType == null) {
headers.add(new PrivateMessageHeader(id, g, timestamp,
local, read, s.isSent(), s.isSeen(), true,
emptyList()));
} else {
boolean hasText = meta.getBoolean(MSG_KEY_HAS_TEXT);
headers.add(new PrivateMessageHeader(id, g, timestamp,
local, read, s.isSent(), s.isSeen(), hasText,
parseAttachmentHeaders(meta)));
}
} catch (FormatException e) { } catch (FormatException e) {
throw new DbException(e); throw new DbException(e);
} }
@@ -257,19 +365,26 @@ class MessagingManagerImpl extends ConversationClientImpl
@Override @Override
public String getMessageText(MessageId m) throws DbException { public String getMessageText(MessageId m) throws DbException {
try { try {
// 0: private message text BdfList body = clientHelper.getMessageAsList(m);
return clientHelper.getMessageAsList(m).getString(0); if (body.size() == 1) return body.getString(0); // Legacy format
else return body.getOptionalString(1);
} catch (FormatException e) { } catch (FormatException e) {
throw new DbException(e); throw new DbException(e);
} }
} }
@Override @Override
public Attachment getAttachment(MessageId mId) throws DbException { public Attachment getAttachment(MessageId m) throws DbException {
// TODO add real implementation // TODO: Support large messages
Message m = clientHelper.getMessage(mId); byte[] body = clientHelper.getMessage(m).getBody();
byte[] bytes = m.getBody(); try {
return new Attachment(new ByteArrayInputStream(bytes)); BdfDictionary meta = clientHelper.getMessageMetadataAsDictionary(m);
int offset = meta.getLong(MSG_KEY_DESCRIPTOR_LENGTH).intValue();
return new Attachment(new ByteArrayInputStream(body, offset,
body.length - offset));
} catch (FormatException e) {
throw new DbException(e);
}
} }
@Override @Override
@@ -278,7 +393,7 @@ class MessagingManagerImpl extends ConversationClientImpl
int minorVersion = clientVersioningManager int minorVersion = clientVersioningManager
.getClientMinorVersion(txn, c, CLIENT_ID, 0); .getClientMinorVersion(txn, c, CLIENT_ID, 0);
// support was added in 0.1 // support was added in 0.1
return minorVersion == 1; return minorVersion > 0;
} }
} }

View File

@@ -1,7 +1,8 @@
package org.briarproject.briar.messaging; package org.briarproject.briar.messaging;
import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.FeatureFlags;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.data.BdfReaderFactory;
import org.briarproject.bramble.api.data.MetadataEncoder; import org.briarproject.bramble.api.data.MetadataEncoder;
import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.sync.validation.ValidationManager; import org.briarproject.bramble.api.sync.validation.ValidationManager;
@@ -19,7 +20,6 @@ import dagger.Provides;
import static org.briarproject.briar.api.messaging.MessagingManager.CLIENT_ID; import static org.briarproject.briar.api.messaging.MessagingManager.CLIENT_ID;
import static org.briarproject.briar.api.messaging.MessagingManager.MAJOR_VERSION; import static org.briarproject.briar.api.messaging.MessagingManager.MAJOR_VERSION;
import static org.briarproject.briar.api.messaging.MessagingManager.MINOR_VERSION;
@Module @Module
public class MessagingModule { public class MessagingModule {
@@ -42,10 +42,10 @@ public class MessagingModule {
@Provides @Provides
@Singleton @Singleton
PrivateMessageValidator getValidator(ValidationManager validationManager, PrivateMessageValidator getValidator(ValidationManager validationManager,
ClientHelper clientHelper, MetadataEncoder metadataEncoder, BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder,
Clock clock) { Clock clock) {
PrivateMessageValidator validator = new PrivateMessageValidator( PrivateMessageValidator validator = new PrivateMessageValidator(
clientHelper, metadataEncoder, clock); bdfReaderFactory, metadataEncoder, clock);
validationManager.registerMessageValidator(CLIENT_ID, MAJOR_VERSION, validationManager.registerMessageValidator(CLIENT_ID, MAJOR_VERSION,
validator); validator);
return validator; return validator;
@@ -57,14 +57,17 @@ public class MessagingModule {
ContactManager contactManager, ValidationManager validationManager, ContactManager contactManager, ValidationManager validationManager,
ConversationManager conversationManager, ConversationManager conversationManager,
ClientVersioningManager clientVersioningManager, ClientVersioningManager clientVersioningManager,
MessagingManagerImpl messagingManager) { FeatureFlags featureFlags, MessagingManagerImpl messagingManager) {
lifecycleManager.registerOpenDatabaseHook(messagingManager); lifecycleManager.registerOpenDatabaseHook(messagingManager);
contactManager.registerContactHook(messagingManager); contactManager.registerContactHook(messagingManager);
validationManager.registerIncomingMessageHook(CLIENT_ID, MAJOR_VERSION, validationManager.registerIncomingMessageHook(CLIENT_ID, MAJOR_VERSION,
messagingManager); messagingManager);
conversationManager.registerConversationClient(messagingManager); conversationManager.registerConversationClient(messagingManager);
// Advertise the current or previous minor version depending on the
// feature flag
int minorVersion = featureFlags.shouldEnableImageAttachments() ? 1 : 0;
clientVersioningManager.registerClient(CLIENT_ID, MAJOR_VERSION, clientVersioningManager.registerClient(CLIENT_ID, MAJOR_VERSION,
MINOR_VERSION, messagingManager); minorVersion, messagingManager);
return messagingManager; return messagingManager;
} }

View File

@@ -12,11 +12,13 @@ import org.briarproject.briar.api.messaging.PrivateMessageFactory;
import java.util.List; import java.util.List;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import javax.inject.Inject; import javax.inject.Inject;
import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong; import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH; import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH;
import static org.briarproject.briar.messaging.MessageTypes.PRIVATE_MESSAGE;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
@@ -30,15 +32,36 @@ class PrivateMessageFactoryImpl implements PrivateMessageFactory {
} }
@Override @Override
public PrivateMessage createPrivateMessage(GroupId groupId, long timestamp, public PrivateMessage createLegacyPrivateMessage(GroupId groupId,
String text, List<AttachmentHeader> attachments) long timestamp, String text) throws FormatException {
throws FormatException {
// Validate the arguments // Validate the arguments
if (utf8IsTooLong(text, MAX_PRIVATE_MESSAGE_TEXT_LENGTH)) if (utf8IsTooLong(text, MAX_PRIVATE_MESSAGE_TEXT_LENGTH))
throw new IllegalArgumentException(); throw new IllegalArgumentException();
// Serialise the message // Serialise the message
BdfList message = BdfList.of(text); BdfList body = BdfList.of(text);
Message m = clientHelper.createMessage(groupId, timestamp, message); Message m = clientHelper.createMessage(groupId, timestamp, body);
return new PrivateMessage(m); return new PrivateMessage(m);
} }
@Override
public PrivateMessage createPrivateMessage(GroupId groupId, long timestamp,
@Nullable String text, List<AttachmentHeader> headers)
throws FormatException {
// Validate the arguments
if (text == null) {
if (headers.isEmpty()) throw new IllegalArgumentException();
} else if (utf8IsTooLong(text, MAX_PRIVATE_MESSAGE_TEXT_LENGTH)) {
throw new IllegalArgumentException();
}
// Serialise the attachment headers
BdfList attachmentList = new BdfList();
for (AttachmentHeader a : headers) {
attachmentList.add(
BdfList.of(a.getMessageId(), a.getContentType()));
}
// Serialise the message
BdfList body = BdfList.of(PRIVATE_MESSAGE, text, attachmentList);
Message m = clientHelper.createMessage(groupId, timestamp, body);
return new PrivateMessage(m, text != null, headers);
}
} }

View File

@@ -1,35 +1,103 @@
package org.briarproject.briar.messaging; package org.briarproject.briar.messaging;
import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.client.BdfMessageContext; import org.briarproject.bramble.api.client.BdfMessageContext;
import org.briarproject.bramble.api.client.BdfMessageValidator;
import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.data.BdfReader;
import org.briarproject.bramble.api.data.BdfReaderFactory;
import org.briarproject.bramble.api.data.MetadataEncoder; import org.briarproject.bramble.api.data.MetadataEncoder;
import org.briarproject.bramble.api.db.Metadata;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Group; import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.sync.InvalidMessageException;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageContext;
import org.briarproject.bramble.api.sync.validation.MessageValidator;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.system.Clock;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
import static org.briarproject.bramble.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
import static org.briarproject.bramble.util.ValidationUtils.checkLength; import static org.briarproject.bramble.util.ValidationUtils.checkLength;
import static org.briarproject.bramble.util.ValidationUtils.checkSize; import static org.briarproject.bramble.util.ValidationUtils.checkSize;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_CONTENT_TYPE_BYTES;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH; import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH;
import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ; import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ;
import static org.briarproject.briar.messaging.MessageTypes.ATTACHMENT;
import static org.briarproject.briar.messaging.MessageTypes.PRIVATE_MESSAGE;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_ATTACHMENT_HEADERS;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_CONTENT_TYPE;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_DESCRIPTOR_LENGTH;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_HAS_TEXT;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_LOCAL;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_MSG_TYPE;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_TIMESTAMP;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
class PrivateMessageValidator extends BdfMessageValidator { class PrivateMessageValidator implements MessageValidator {
PrivateMessageValidator(ClientHelper clientHelper, private final BdfReaderFactory bdfReaderFactory;
private final MetadataEncoder metadataEncoder;
private final Clock clock;
PrivateMessageValidator(BdfReaderFactory bdfReaderFactory,
MetadataEncoder metadataEncoder, Clock clock) { MetadataEncoder metadataEncoder, Clock clock) {
super(clientHelper, metadataEncoder, clock); this.bdfReaderFactory = bdfReaderFactory;
this.metadataEncoder = metadataEncoder;
this.clock = clock;
} }
@Override @Override
protected BdfMessageContext validateMessage(Message m, Group g, public MessageContext validateMessage(Message m, Group g)
throws InvalidMessageException {
// Reject the message if it's too far in the future
long now = clock.currentTimeMillis();
if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) {
throw new InvalidMessageException(
"Timestamp is too far in the future");
}
try {
// TODO: Support large messages
InputStream in = new ByteArrayInputStream(m.getBody());
CountingInputStream countIn =
new CountingInputStream(in, MAX_MESSAGE_BODY_LENGTH);
BdfReader reader = bdfReaderFactory.createReader(countIn);
BdfList list = reader.readList();
long bytesRead = countIn.getBytesRead();
BdfMessageContext context;
if (list.size() == 1) {
// Legacy private message
if (!reader.eof()) throw new FormatException();
context = validateLegacyPrivateMessage(m, list);
} else {
// Private message or attachment
int messageType = list.getLong(0).intValue();
if (messageType == PRIVATE_MESSAGE) {
if (!reader.eof()) throw new FormatException();
context = validatePrivateMessage(m, list);
} else if (messageType == ATTACHMENT) {
context = validateAttachment(m, list, bytesRead);
} else {
throw new InvalidMessageException();
}
}
Metadata meta = metadataEncoder.encode(context.getDictionary());
return new MessageContext(meta, context.getDependencies());
} catch (IOException e) {
throw new InvalidMessageException(e);
}
}
private BdfMessageContext validateLegacyPrivateMessage(Message m,
BdfList body) throws FormatException { BdfList body) throws FormatException {
// Private message text // Private message text
checkSize(body, 1); checkSize(body, 1);
@@ -37,9 +105,54 @@ class PrivateMessageValidator extends BdfMessageValidator {
checkLength(text, 0, MAX_PRIVATE_MESSAGE_TEXT_LENGTH); checkLength(text, 0, MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
// Return the metadata // Return the metadata
BdfDictionary meta = new BdfDictionary(); BdfDictionary meta = new BdfDictionary();
meta.put("timestamp", m.getTimestamp()); meta.put(MSG_KEY_TIMESTAMP, m.getTimestamp());
meta.put("local", false); meta.put(MSG_KEY_LOCAL, false);
meta.put(MSG_KEY_READ, false); meta.put(MSG_KEY_READ, false);
return new BdfMessageContext(meta); return new BdfMessageContext(meta);
} }
private BdfMessageContext validatePrivateMessage(Message m, BdfList body)
throws FormatException {
// Message type, optional private message text, attachment headers
checkSize(body, 3);
String text = body.getOptionalString(1);
checkLength(text, 0, MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
BdfList headers = body.getList(2);
if (text == null) checkSize(headers, 1, MAX_ATTACHMENTS_PER_MESSAGE);
else checkSize(headers, 0, MAX_ATTACHMENTS_PER_MESSAGE);
for (int i = 0; i < headers.size(); i++) {
BdfList header = headers.getList(i);
// Message ID, content type
checkSize(header, 2);
byte[] id = header.getRaw(0);
checkLength(id, UniqueId.LENGTH);
String contentType = header.getString(1);
checkLength(contentType, 1, MAX_CONTENT_TYPE_BYTES);
}
// Return the metadata
BdfDictionary meta = new BdfDictionary();
meta.put(MSG_KEY_TIMESTAMP, m.getTimestamp());
meta.put(MSG_KEY_LOCAL, false);
meta.put(MSG_KEY_READ, false);
meta.put(MSG_KEY_MSG_TYPE, PRIVATE_MESSAGE);
meta.put(MSG_KEY_HAS_TEXT, text != null);
meta.put(MSG_KEY_ATTACHMENT_HEADERS, headers);
return new BdfMessageContext(meta);
}
private BdfMessageContext validateAttachment(Message m, BdfList descriptor,
long descriptorLength) throws FormatException {
// Message type, content type
checkSize(descriptor, 2);
String contentType = descriptor.getString(1);
checkLength(contentType, 1, MAX_CONTENT_TYPE_BYTES);
// Return the metadata
BdfDictionary meta = new BdfDictionary();
meta.put(MSG_KEY_TIMESTAMP, m.getTimestamp());
meta.put(MSG_KEY_LOCAL, false);
meta.put(MSG_KEY_MSG_TYPE, ATTACHMENT);
meta.put(MSG_KEY_DESCRIPTOR_LENGTH, descriptorLength);
meta.put(MSG_KEY_CONTENT_TYPE, contentType);
return new BdfMessageContext(meta);
}
} }

View File

@@ -138,7 +138,7 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
meta.put(KEY_TYPE, JOIN.getInt()); meta.put(KEY_TYPE, JOIN.getInt());
meta.put(KEY_INITIAL_JOIN_MSG, creator); meta.put(KEY_INITIAL_JOIN_MSG, creator);
addMessageMetadata(meta, m); addMessageMetadata(meta, m);
clientHelper.addLocalMessage(txn, m.getMessage(), meta, true); clientHelper.addLocalMessage(txn, m.getMessage(), meta, true, false);
messageTracker.trackOutgoingMessage(txn, m.getMessage()); messageTracker.trackOutgoingMessage(txn, m.getMessage());
addMember(txn, m.getMessage().getGroupId(), m.getMember(), VISIBLE); addMember(txn, m.getMessage().getGroupId(), m.getMember(), VISIBLE);
setPreviousMsgId(txn, m.getMessage().getGroupId(), setPreviousMsgId(txn, m.getMessage().getGroupId(),
@@ -217,7 +217,8 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
meta.put(KEY_PARENT_MSG_ID, m.getParent()); meta.put(KEY_PARENT_MSG_ID, m.getParent());
addMessageMetadata(meta, m); addMessageMetadata(meta, m);
GroupId g = m.getMessage().getGroupId(); GroupId g = m.getMessage().getGroupId();
clientHelper.addLocalMessage(txn, m.getMessage(), meta, true); clientHelper.addLocalMessage(txn, m.getMessage(), meta, true,
false);
// track message // track message
setPreviousMsgId(txn, g, m.getMessage().getId()); setPreviousMsgId(txn, g, m.getMessage().getId());

View File

@@ -93,7 +93,7 @@ abstract class AbstractProtocolEngine<S extends Session>
boolean isValidDependency(S session, @Nullable MessageId dependency) { boolean isValidDependency(S session, @Nullable MessageId dependency) {
MessageId expected = session.getLastRemoteMessageId(); MessageId expected = session.getLastRemoteMessageId();
if (dependency == null) return expected == null; if (dependency == null) return expected == null;
return expected != null && dependency.equals(expected); return dependency.equals(expected);
} }
void setPrivateGroupVisibility(Transaction txn, S session, void setPrivateGroupVisibility(Transaction txn, S session,
@@ -223,7 +223,7 @@ abstract class AbstractProtocolEngine<S extends Session>
.encodeMetadata(type, privateGroupId, m.getTimestamp(), true, .encodeMetadata(type, privateGroupId, m.getTimestamp(), true,
true, visibleInConversation, false, false); true, visibleInConversation, false, false);
try { try {
clientHelper.addLocalMessage(txn, m, meta, true); clientHelper.addLocalMessage(txn, m, meta, true, false);
} catch (FormatException e) { } catch (FormatException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }

View File

@@ -567,7 +567,7 @@ abstract class ProtocolEngineImpl<S extends Shareable>
.encodeMetadata(type, shareableId, m.getTimestamp(), true, true, .encodeMetadata(type, shareableId, m.getTimestamp(), true, true,
visibleInConversation, false, false); visibleInConversation, false, false);
try { try {
clientHelper.addLocalMessage(txn, m, meta, true); clientHelper.addLocalMessage(txn, m, meta, true, false);
} catch (FormatException e) { } catch (FormatException e) {
throw new AssertionError(e); throw new AssertionError(e);
} }
@@ -627,7 +627,7 @@ abstract class ProtocolEngineImpl<S extends Shareable>
@Nullable MessageId dependency) { @Nullable MessageId dependency) {
MessageId expected = session.getLastRemoteMessageId(); MessageId expected = session.getLastRemoteMessageId();
if (dependency == null) return expected != null; if (dependency == null) return expected != null;
return expected == null || !dependency.equals(expected); return !dependency.equals(expected);
} }
private long getLocalTimestamp(Session session) { private long getLocalTimestamp(Session session) {

View File

@@ -325,7 +325,8 @@ public class TestDataCreatorImpl implements TestDataCreator {
Transaction txn = db.startTransaction(false); Transaction txn = db.startTransaction(false);
try { try {
clientHelper.addLocalMessage(txn, m.getMessage(), meta, true); clientHelper.addLocalMessage(txn, m.getMessage(), meta, true,
false);
if (local) messageTracker.trackOutgoingMessage(txn, m.getMessage()); if (local) messageTracker.trackOutgoingMessage(txn, m.getMessage());
else messageTracker.trackIncomingMessage(txn, m.getMessage()); else messageTracker.trackIncomingMessage(txn, m.getMessage());
db.commitTransaction(txn); db.commitTransaction(txn);
@@ -334,13 +335,6 @@ public class TestDataCreatorImpl implements TestDataCreator {
} }
} }
@Override
public void addPrivateMessage(Contact contact, String text, long time,
boolean local) throws DbException, FormatException {
Group group = messagingManager.getContactGroup(contact);
createPrivateMessage(group.getId(), text, time, local);
}
private void createBlogPosts(List<Contact> contacts, int numBlogPosts) private void createBlogPosts(List<Contact> contacts, int numBlogPosts)
throws DbException { throws DbException {
for (int i = 0; i < numBlogPosts; i++) { for (int i = 0; i < numBlogPosts; i++) {

View File

@@ -287,7 +287,8 @@ public class BlogManagerImplTest extends BriarTestCase {
will(returnValue(blog1)); will(returnValue(blog1));
oneOf(clientHelper).toList(localAuthor1); oneOf(clientHelper).toList(localAuthor1);
will(returnValue(authorList1)); will(returnValue(authorList1));
oneOf(clientHelper).addLocalMessage(txn, message, meta, true); oneOf(clientHelper).addLocalMessage(txn, message, meta, true,
false);
oneOf(clientHelper).parseAndValidateAuthor(authorList1); oneOf(clientHelper).parseAndValidateAuthor(authorList1);
will(returnValue(localAuthor1)); will(returnValue(localAuthor1));
oneOf(contactManager).getAuthorInfo(txn, localAuthor1.getId()); oneOf(contactManager).getAuthorInfo(txn, localAuthor1.getId());
@@ -340,7 +341,8 @@ public class BlogManagerImplTest extends BriarTestCase {
will(returnValue(rssBlog)); will(returnValue(rssBlog));
oneOf(clientHelper).toList(rssLocalAuthor); oneOf(clientHelper).toList(rssLocalAuthor);
will(returnValue(rssAuthorList)); will(returnValue(rssAuthorList));
oneOf(clientHelper).addLocalMessage(txn, rssMessage, meta, true); oneOf(clientHelper).addLocalMessage(txn, rssMessage, meta, true,
false);
oneOf(clientHelper).parseAndValidateAuthor(rssAuthorList); oneOf(clientHelper).parseAndValidateAuthor(rssAuthorList);
will(returnValue(rssLocalAuthor)); will(returnValue(rssLocalAuthor));
oneOf(db).commitTransaction(txn); oneOf(db).commitTransaction(txn);
@@ -407,7 +409,7 @@ public class BlogManagerImplTest extends BriarTestCase {
will(returnValue(authorList1)); will(returnValue(authorList1));
// Store the comment // Store the comment
oneOf(clientHelper).addLocalMessage(txn, commentMsg, commentMeta, oneOf(clientHelper).addLocalMessage(txn, commentMsg, commentMeta,
true); true, false);
// Create the headers for the comment and its parent // Create the headers for the comment and its parent
oneOf(clientHelper).parseAndValidateAuthor(authorList1); oneOf(clientHelper).parseAndValidateAuthor(authorList1);
will(returnValue(localAuthor1)); will(returnValue(localAuthor1));
@@ -508,7 +510,7 @@ public class BlogManagerImplTest extends BriarTestCase {
will(returnValue(authorList1)); will(returnValue(authorList1));
// Store the wrapped post // Store the wrapped post
oneOf(clientHelper).addLocalMessage(txn, wrappedPostMsg, oneOf(clientHelper).addLocalMessage(txn, wrappedPostMsg,
wrappedPostMeta, true); wrappedPostMeta, true, false);
// Create the comment // Create the comment
oneOf(blogPostFactory).createBlogComment(blog2.getId(), oneOf(blogPostFactory).createBlogComment(blog2.getId(),
localAuthor2, comment, messageId, wrappedPostId); localAuthor2, comment, messageId, wrappedPostId);
@@ -517,7 +519,7 @@ public class BlogManagerImplTest extends BriarTestCase {
will(returnValue(authorList2)); will(returnValue(authorList2));
// Store the comment // Store the comment
oneOf(clientHelper).addLocalMessage(txn, commentMsg, commentMeta, oneOf(clientHelper).addLocalMessage(txn, commentMsg, commentMeta,
true); true, false);
// Create the headers for the comment and the wrapped post // Create the headers for the comment and the wrapped post
oneOf(clientHelper).parseAndValidateAuthor(authorList2); oneOf(clientHelper).parseAndValidateAuthor(authorList2);
will(returnValue(localAuthor2)); will(returnValue(localAuthor2));
@@ -619,7 +621,7 @@ public class BlogManagerImplTest extends BriarTestCase {
will(returnValue(rssAuthorList)); will(returnValue(rssAuthorList));
// Store the wrapped post // Store the wrapped post
oneOf(clientHelper).addLocalMessage(txn, wrappedPostMsg, oneOf(clientHelper).addLocalMessage(txn, wrappedPostMsg,
wrappedPostMeta, true); wrappedPostMeta, true, false);
// Create the comment // Create the comment
oneOf(blogPostFactory).createBlogComment(blog1.getId(), oneOf(blogPostFactory).createBlogComment(blog1.getId(),
localAuthor1, comment, rssMessageId, wrappedPostId); localAuthor1, comment, rssMessageId, wrappedPostId);
@@ -628,7 +630,7 @@ public class BlogManagerImplTest extends BriarTestCase {
will(returnValue(authorList1)); will(returnValue(authorList1));
// Store the comment // Store the comment
oneOf(clientHelper).addLocalMessage(txn, commentMsg, commentMeta, oneOf(clientHelper).addLocalMessage(txn, commentMsg, commentMeta,
true); true, false);
// Create the headers for the comment and the wrapped post // Create the headers for the comment and the wrapped post
oneOf(clientHelper).parseAndValidateAuthor(authorList1); oneOf(clientHelper).parseAndValidateAuthor(authorList1);
will(returnValue(localAuthor1)); will(returnValue(localAuthor1));
@@ -741,7 +743,7 @@ public class BlogManagerImplTest extends BriarTestCase {
will(returnValue(rssAuthorList)); will(returnValue(rssAuthorList));
// Store the rewrapped post // Store the rewrapped post
oneOf(clientHelper).addLocalMessage(txn, rewrappedPostMsg, oneOf(clientHelper).addLocalMessage(txn, rewrappedPostMsg,
rewrappedPostMeta, true); rewrappedPostMeta, true, false);
// Wrap the original comment for blog 2 // Wrap the original comment for blog 2
oneOf(clientHelper).getMessageAsList(txn, originalCommentId); oneOf(clientHelper).getMessageAsList(txn, originalCommentId);
will(returnValue(originalCommentBody)); will(returnValue(originalCommentBody));
@@ -758,7 +760,7 @@ public class BlogManagerImplTest extends BriarTestCase {
will(returnValue(authorList1)); will(returnValue(authorList1));
// Store the wrapped comment // Store the wrapped comment
oneOf(clientHelper).addLocalMessage(txn, wrappedCommentMsg, oneOf(clientHelper).addLocalMessage(txn, wrappedCommentMsg,
wrappedCommentMeta, true); wrappedCommentMeta, true, false);
// Create the new comment // Create the new comment
oneOf(blogPostFactory).createBlogComment(blog2.getId(), oneOf(blogPostFactory).createBlogComment(blog2.getId(),
localAuthor2, localComment, originalCommentId, localAuthor2, localComment, originalCommentId,
@@ -768,7 +770,7 @@ public class BlogManagerImplTest extends BriarTestCase {
will(returnValue(authorList2)); will(returnValue(authorList2));
// Store the new comment // Store the new comment
oneOf(clientHelper).addLocalMessage(txn, localCommentMsg, oneOf(clientHelper).addLocalMessage(txn, localCommentMsg,
localCommentMeta, true); localCommentMeta, true, false);
// Create the headers for the new comment, the wrapped comment and // Create the headers for the new comment, the wrapped comment and
// the rewrapped post // the rewrapped post
oneOf(clientHelper).parseAndValidateAuthor(authorList2); oneOf(clientHelper).parseAndValidateAuthor(authorList2);

View File

@@ -1075,8 +1075,8 @@ public class IntroductionIntegrationTest
m.getPreviousMessageId(), m.getSessionId(), m.getPreviousMessageId(), m.getSessionId(),
m.getEphemeralPublicKey(), m.getAcceptTimestamp(), m.getEphemeralPublicKey(), m.getAcceptTimestamp(),
m.getTransportProperties()); m.getTransportProperties());
c0.getClientHelper() c0.getClientHelper().addLocalMessage(txn, msg, new BdfDictionary(),
.addLocalMessage(txn, msg, new BdfDictionary(), true); true, false);
Group group0 = getLocalGroup(); Group group0 = getLocalGroup();
BdfDictionary query = BdfDictionary.of( BdfDictionary query = BdfDictionary.of(
new BdfEntry(SESSION_KEY_SESSION_ID, m.getSessionId()) new BdfEntry(SESSION_KEY_SESSION_ID, m.getSessionId())

View File

@@ -0,0 +1,185 @@
package org.briarproject.briar.messaging;
import org.briarproject.bramble.test.BrambleTestCase;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Random;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
public class CountingInputStreamTest extends BrambleTestCase {
private final Random random = new Random();
private final byte[] src = getRandomBytes(123);
@Test
public void testCountsSingleByteReads() throws Exception {
InputStream delegate = new ByteArrayInputStream(src);
// The limit is high enough to read the whole src array
CountingInputStream in =
new CountingInputStream(delegate, src.length + 1);
// No bytes should have been read initially
assertEquals(0L, in.getBytesRead());
// The reads should return the contents of the src array
for (int i = 0; i < src.length; i++) {
assertEquals(i, in.getBytesRead());
assertEquals(src[i] & 0xFF, in.read());
}
// The count should match the length of the src array
assertEquals(src.length, in.getBytesRead());
// Trying to read another byte should return EOF
assertEquals(-1, in.read());
// Reading EOF shouldn't affect the count
assertEquals(src.length, in.getBytesRead());
}
@Test
public void testCountsMultiByteReads() throws Exception {
InputStream delegate = new ByteArrayInputStream(src);
// The limit is high enough to read the whole src array
CountingInputStream in =
new CountingInputStream(delegate, src.length + 1);
// No bytes should have been read initially
assertEquals(0L, in.getBytesRead());
// Copy the src array in random-sized pieces
byte[] dest = new byte[src.length];
int offset = 0;
while (offset < dest.length) {
assertEquals(offset, in.getBytesRead());
int length = Math.min(random.nextInt(10), dest.length - offset);
assertEquals(length, in.read(dest, offset, length));
offset += length;
}
// The dest array should be a copy of the src array
assertArrayEquals(src, dest);
// The count should match the length of the src array
assertEquals(src.length, in.getBytesRead());
// Trying to read another byte should return EOF
assertEquals(-1, in.read(dest, 0, 1));
// Reading EOF shouldn't affect the count
assertEquals(src.length, in.getBytesRead());
}
@Test
public void testCountsSkips() throws Exception {
InputStream delegate = new ByteArrayInputStream(src);
// The limit is high enough to read the whole src array
CountingInputStream in =
new CountingInputStream(delegate, src.length + 1);
// No bytes should have been read initially
assertEquals(0L, in.getBytesRead());
// Skip the src array in random-sized pieces
int offset = 0;
while (offset < src.length) {
assertEquals(offset, in.getBytesRead());
int length = Math.min(random.nextInt(10), src.length - offset);
assertEquals(length, in.skip(length));
offset += length;
}
// The count should match the length of the src array
assertEquals(src.length, in.getBytesRead());
// Trying to skip another byte should return zero
assertEquals(0, in.skip(1));
// Returning zero shouldn't affect the count
assertEquals(src.length, in.getBytesRead());
}
@Test
public void testReturnsEofWhenSingleByteReadReachesLimit()
throws Exception {
InputStream delegate = new ByteArrayInputStream(src);
// The limit is one byte lower than the length of the src array
CountingInputStream in =
new CountingInputStream(delegate, src.length - 1);
// The reads should return the contents of the src array, except the
// last byte
for (int i = 0; i < src.length - 1; i++) {
assertEquals(src[i] & 0xFF, in.read());
}
// The count should match the limit
assertEquals(src.length - 1, in.getBytesRead());
// Trying to read another byte should return EOF
assertEquals(-1, in.read());
// Reading EOF shouldn't affect the count
assertEquals(src.length - 1, in.getBytesRead());
}
@Test
public void testReturnsEofWhenMultiByteReadReachesLimit() throws Exception {
InputStream delegate = new ByteArrayInputStream(src);
// The limit is one byte lower than the length of the src array
CountingInputStream in =
new CountingInputStream(delegate, src.length - 1);
// Copy the src array in random-sized pieces, except the last two bytes
byte[] dest = new byte[src.length];
int offset = 0;
while (offset < dest.length - 2) {
int length = Math.min(random.nextInt(10), dest.length - 2 - offset);
assertEquals(length, in.read(dest, offset, length));
offset += length;
}
// Trying to read two bytes should only return one, reaching the limit
assertEquals(1, in.read(dest, offset, 2));
// The dest array should be a copy of the src array, except the last
// byte
for (int i = 0; i < src.length - 1; i++) assertEquals(src[i], dest[i]);
// The count should match the limit
assertEquals(src.length - 1, in.getBytesRead());
// Trying to read another byte should return EOF
assertEquals(-1, in.read(dest, 0, 1));
// Reading EOF shouldn't affect the count
assertEquals(src.length - 1, in.getBytesRead());
}
@Test
public void testReturnsZeroWhenSkipReachesLimit() throws Exception {
InputStream delegate = new ByteArrayInputStream(src);
// The limit is one byte lower than the length of the src array
CountingInputStream in =
new CountingInputStream(delegate, src.length - 1);
// Skip the src array in random-sized pieces, except the last two bytes
int offset = 0;
while (offset < src.length - 2) {
assertEquals(offset, in.getBytesRead());
int length = Math.min(random.nextInt(10), src.length - 2 - offset);
assertEquals(length, in.skip(length));
offset += length;
}
// Trying to skip two bytes should only skip one, reaching the limit
assertEquals(1, in.skip(2));
// The count should match the limit
assertEquals(src.length - 1, in.getBytesRead());
// Trying to skip another byte should return zero
assertEquals(0, in.skip(1));
// Returning zero shouldn't affect the count
assertEquals(src.length - 1, in.getBytesRead());
}
@Test
public void testMarkIsNotSupported() {
InputStream delegate = new ByteArrayInputStream(src);
CountingInputStream in = new CountingInputStream(delegate, src.length);
assertFalse(in.markSupported());
}
@Test(expected = IOException.class)
public void testResetIsNotSupported() throws Exception {
InputStream delegate = new ByteArrayInputStream(src);
CountingInputStream in = new CountingInputStream(delegate, src.length);
in.mark(src.length);
assertEquals(src.length, in.read(new byte[src.length]));
in.reset();
}
}

View File

@@ -8,20 +8,25 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.forum.ForumPost; import org.briarproject.briar.api.forum.ForumPost;
import org.briarproject.briar.api.forum.ForumPostFactory; import org.briarproject.briar.api.forum.ForumPostFactory;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.PrivateMessage; import org.briarproject.briar.api.messaging.PrivateMessage;
import org.briarproject.briar.api.messaging.PrivateMessageFactory; import org.briarproject.briar.api.messaging.PrivateMessageFactory;
import org.briarproject.briar.test.BriarTestCase; import org.briarproject.briar.test.BriarTestCase;
import org.junit.Test; import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import static java.util.Collections.emptyList;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
import static org.briarproject.bramble.api.record.Record.MAX_RECORD_PAYLOAD_BYTES; import static org.briarproject.bramble.api.record.Record.MAX_RECORD_PAYLOAD_BYTES;
import static org.briarproject.bramble.test.TestUtils.getRandomId; import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.util.StringUtils.getRandomString; import static org.briarproject.bramble.util.StringUtils.getRandomString;
import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_TEXT_LENGTH; import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_TEXT_LENGTH;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_CONTENT_TYPE_BYTES;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH; import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@@ -43,18 +48,40 @@ public class MessageSizeIntegrationTest extends BriarTestCase {
component.inject(this); component.inject(this);
} }
@Test
public void testLegacyPrivateMessageFitsIntoRecord() throws Exception {
// Create a maximum-length private message
GroupId groupId = new GroupId(getRandomId());
long timestamp = Long.MAX_VALUE;
String text = getRandomString(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
PrivateMessage message = privateMessageFactory
.createLegacyPrivateMessage(groupId, timestamp, text);
// Check the size of the serialised message
int length = message.getMessage().getRawLength();
assertTrue(length > UniqueId.LENGTH + 8
+ MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
assertTrue(length <= MAX_RECORD_PAYLOAD_BYTES);
}
@Test @Test
public void testPrivateMessageFitsIntoRecord() throws Exception { public void testPrivateMessageFitsIntoRecord() throws Exception {
// Create a maximum-length private message // Create a maximum-length private message
GroupId groupId = new GroupId(getRandomId()); GroupId groupId = new GroupId(getRandomId());
long timestamp = Long.MAX_VALUE; long timestamp = Long.MAX_VALUE;
String text = getRandomString(MAX_PRIVATE_MESSAGE_TEXT_LENGTH); String text = getRandomString(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
// Create the maximum number of maximum-length attachment headers
List<AttachmentHeader> headers = new ArrayList<>();
for (int i = 0; i < MAX_ATTACHMENTS_PER_MESSAGE; i++) {
headers.add(new AttachmentHeader(new MessageId(getRandomId()),
getRandomString(MAX_CONTENT_TYPE_BYTES)));
}
PrivateMessage message = privateMessageFactory.createPrivateMessage( PrivateMessage message = privateMessageFactory.createPrivateMessage(
groupId, timestamp, text, emptyList()); groupId, timestamp, text, headers);
// Check the size of the serialised message // Check the size of the serialised message
int length = message.getMessage().getRawLength(); int length = message.getMessage().getRawLength();
assertTrue(length > UniqueId.LENGTH + 8 assertTrue(length > UniqueId.LENGTH + 8
+ MAX_PRIVATE_MESSAGE_TEXT_LENGTH); + MAX_PRIVATE_MESSAGE_TEXT_LENGTH + MAX_ATTACHMENTS_PER_MESSAGE
* (UniqueId.LENGTH + MAX_CONTENT_TYPE_BYTES));
assertTrue(length <= MAX_RECORD_PAYLOAD_BYTES); assertTrue(length <= MAX_RECORD_PAYLOAD_BYTES);
} }

View File

@@ -1,83 +1,472 @@
package org.briarproject.briar.messaging; package org.briarproject.briar.messaging;
import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.UniqueId;
import org.briarproject.bramble.api.client.BdfMessageContext;
import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfEntry;
import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.test.ValidatorTestCase; import org.briarproject.bramble.api.data.BdfReader;
import org.briarproject.bramble.api.data.BdfReaderFactory;
import org.briarproject.bramble.api.data.MetadataEncoder;
import org.briarproject.bramble.api.db.Metadata;
import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.sync.InvalidMessageException;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageContext;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.jmock.Expectations;
import org.junit.Test; import org.junit.Test;
import java.io.InputStream;
import static org.briarproject.bramble.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
import static org.briarproject.bramble.test.TestUtils.getClientId;
import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getMessage;
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.briarproject.bramble.util.StringUtils.getRandomString;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_CONTENT_TYPE_BYTES;
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH; import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH;
import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ; import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ;
import static org.briarproject.briar.messaging.MessageTypes.ATTACHMENT;
import static org.briarproject.briar.messaging.MessageTypes.PRIVATE_MESSAGE;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_ATTACHMENT_HEADERS;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_CONTENT_TYPE;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_DESCRIPTOR_LENGTH;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_HAS_TEXT;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_LOCAL;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_MSG_TYPE;
import static org.briarproject.briar.messaging.MessagingConstants.MSG_KEY_TIMESTAMP;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
public class PrivateMessageValidatorTest extends ValidatorTestCase { public class PrivateMessageValidatorTest extends BrambleMockTestCase {
@Test(expected = FormatException.class) private final BdfReaderFactory bdfReaderFactory =
context.mock(BdfReaderFactory.class);
private final MetadataEncoder metadataEncoder =
context.mock(MetadataEncoder.class);
private final Clock clock = context.mock(Clock.class);
private final BdfReader reader = context.mock(BdfReader.class);
private final Group group = getGroup(getClientId(), 123);
private final Message message = getMessage(group.getId());
private final long now = message.getTimestamp() + 1000;
private final String text =
getRandomString(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
private final BdfList attachmentHeader = getAttachmentHeader();
private final MessageId attachmentId = new MessageId(getRandomId());
private final String contentType = getRandomString(MAX_CONTENT_TYPE_BYTES);
private final BdfDictionary legacyMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_TIMESTAMP, message.getTimestamp()),
new BdfEntry(MSG_KEY_LOCAL, false),
new BdfEntry(MSG_KEY_READ, false)
);
private final BdfDictionary noAttachmentsMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_TIMESTAMP, message.getTimestamp()),
new BdfEntry(MSG_KEY_LOCAL, false),
new BdfEntry(MSG_KEY_READ, false),
new BdfEntry(MSG_KEY_MSG_TYPE, PRIVATE_MESSAGE),
new BdfEntry(MSG_KEY_HAS_TEXT, true),
new BdfEntry(MSG_KEY_ATTACHMENT_HEADERS, new BdfList())
);
private final BdfDictionary noTextMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_TIMESTAMP, message.getTimestamp()),
new BdfEntry(MSG_KEY_LOCAL, false),
new BdfEntry(MSG_KEY_READ, false),
new BdfEntry(MSG_KEY_MSG_TYPE, PRIVATE_MESSAGE),
new BdfEntry(MSG_KEY_HAS_TEXT, false),
new BdfEntry(MSG_KEY_ATTACHMENT_HEADERS,
BdfList.of(attachmentHeader))
);
private final BdfDictionary attachmentMeta = BdfDictionary.of(
new BdfEntry(MSG_KEY_TIMESTAMP, message.getTimestamp()),
new BdfEntry(MSG_KEY_LOCAL, false),
new BdfEntry(MSG_KEY_MSG_TYPE, ATTACHMENT),
// Descriptor length is zero as the test doesn't read from the
// counting input stream
new BdfEntry(MSG_KEY_DESCRIPTOR_LENGTH, 0L),
new BdfEntry(MSG_KEY_CONTENT_TYPE, contentType)
);
private final PrivateMessageValidator validator =
new PrivateMessageValidator(bdfReaderFactory, metadataEncoder,
clock);
@Test(expected = InvalidMessageException.class)
public void testRejectsFarFutureTimestamp() throws Exception {
expectCheckTimestamp(message.getTimestamp() - MAX_CLOCK_DIFFERENCE - 1);
validator.validateMessage(message, group);
}
@Test(expected = InvalidMessageException.class)
public void testRejectsTooShortBody() throws Exception { public void testRejectsTooShortBody() throws Exception {
PrivateMessageValidator v = new PrivateMessageValidator(clientHelper, expectCheckTimestamp(now);
metadataEncoder, clock); expectParseList(new BdfList());
v.validateMessage(message, group, new BdfList());
validator.validateMessage(message, group);
} }
@Test(expected = FormatException.class) @Test(expected = InvalidMessageException.class)
public void testRejectsTooLongBody() throws Exception { public void testRejectsTrailingDataForLegacyMessage() throws Exception {
PrivateMessageValidator v = new PrivateMessageValidator(clientHelper, expectCheckTimestamp(now);
metadataEncoder, clock); expectParseList(BdfList.of(text));
v.validateMessage(message, group, BdfList.of("", 123)); expectReadEof(false);
validator.validateMessage(message, group);
} }
@Test(expected = FormatException.class) @Test(expected = InvalidMessageException.class)
public void testRejectsNullText() throws Exception { public void testRejectsNullTextForLegacyMessage() throws Exception {
PrivateMessageValidator v = new PrivateMessageValidator(clientHelper, testRejectsLegacyMessage(BdfList.of((String) null));
metadataEncoder, clock);
v.validateMessage(message, group, BdfList.of((String) null));
} }
@Test(expected = FormatException.class) @Test(expected = InvalidMessageException.class)
public void testRejectsNonStringText() throws Exception { public void testRejectsNonStringTextForLegacyMessage() throws Exception {
PrivateMessageValidator v = new PrivateMessageValidator(clientHelper, testRejectsLegacyMessage(BdfList.of(123));
metadataEncoder, clock);
v.validateMessage(message, group, BdfList.of(123));
} }
@Test(expected = FormatException.class) @Test(expected = InvalidMessageException.class)
public void testRejectsTooLongText() throws Exception { public void testRejectsTooLongTextForLegacyMessage() throws Exception {
PrivateMessageValidator v = new PrivateMessageValidator(clientHelper,
metadataEncoder, clock);
String invalidText = String invalidText =
getRandomString(MAX_PRIVATE_MESSAGE_TEXT_LENGTH + 1); getRandomString(MAX_PRIVATE_MESSAGE_TEXT_LENGTH + 1);
v.validateMessage(message, group, BdfList.of(invalidText));
testRejectsLegacyMessage(BdfList.of(invalidText));
} }
@Test @Test
public void testAcceptsMaxLengthText() throws Exception { public void testAcceptsMaxLengthTextForLegacyMessage() throws Exception {
PrivateMessageValidator v = new PrivateMessageValidator(clientHelper, testAcceptsLegacyMessage(BdfList.of(text));
metadataEncoder, clock);
String text = getRandomString(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
BdfMessageContext messageContext =
v.validateMessage(message, group, BdfList.of(text));
assertExpectedContext(messageContext);
} }
@Test @Test
public void testAcceptsMinLengthText() throws Exception { public void testAcceptsMinLengthTextForLegacyMessage() throws Exception {
PrivateMessageValidator v = new PrivateMessageValidator(clientHelper, testAcceptsLegacyMessage(BdfList.of(""));
metadataEncoder, clock);
BdfMessageContext messageContext =
v.validateMessage(message, group, BdfList.of(""));
assertExpectedContext(messageContext);
} }
private void assertExpectedContext(BdfMessageContext messageContext) @Test(expected = InvalidMessageException.class)
throws FormatException { public void testRejectsTrailingDataForPrivateMessage() throws Exception {
BdfDictionary meta = messageContext.getDictionary(); expectCheckTimestamp(now);
assertEquals(3, meta.size()); expectParseList(BdfList.of(PRIVATE_MESSAGE, text, new BdfList()));
assertEquals(timestamp, meta.getLong("timestamp").longValue()); expectReadEof(false);
assertFalse(meta.getBoolean("local"));
assertFalse(meta.getBoolean(MSG_KEY_READ)); validator.validateMessage(message, group);
assertEquals(0, messageContext.getDependencies().size()); }
@Test(expected = InvalidMessageException.class)
public void testRejectsTooShortBodyForPrivateMessage() throws Exception {
testRejectsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text));
}
@Test(expected = InvalidMessageException.class)
public void testRejectsTooLongBodyForPrivateMessage() throws Exception {
testRejectsPrivateMessage(
BdfList.of(PRIVATE_MESSAGE, text, new BdfList(), 123));
}
@Test(expected = InvalidMessageException.class)
public void testRejectsNullTextWithoutAttachmentsForPrivateMessage()
throws Exception {
testRejectsPrivateMessage(
BdfList.of(PRIVATE_MESSAGE, null, new BdfList()));
}
@Test
public void testAcceptsNullTextWithAttachmentsForPrivateMessage()
throws Exception {
testAcceptsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, null,
BdfList.of(attachmentHeader)), noTextMeta);
}
@Test(expected = InvalidMessageException.class)
public void testRejectsNonStringTextForPrivateMessage() throws Exception {
testRejectsPrivateMessage(
BdfList.of(PRIVATE_MESSAGE, 123, new BdfList()));
}
@Test(expected = InvalidMessageException.class)
public void testRejectsTooLongTextForPrivateMessage() throws Exception {
String invalidText =
getRandomString(MAX_PRIVATE_MESSAGE_TEXT_LENGTH + 1);
testRejectsPrivateMessage(
BdfList.of(PRIVATE_MESSAGE, invalidText, new BdfList()));
}
@Test
public void testAcceptsMaxLengthTextForPrivateMessage() throws Exception {
testAcceptsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text,
new BdfList()), noAttachmentsMeta);
}
@Test
public void testAcceptsMinLengthTextForPrivateMessage() throws Exception {
testAcceptsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, "",
new BdfList()), noAttachmentsMeta);
}
@Test(expected = InvalidMessageException.class)
public void testRejectsNullAttachmentListForPrivateMessage()
throws Exception {
testRejectsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text, null));
}
@Test(expected = InvalidMessageException.class)
public void testRejectsNonListAttachmentListForPrivateMessage()
throws Exception {
testRejectsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text, 123));
}
@Test(expected = InvalidMessageException.class)
public void testRejectsTooLongAttachmentListForPrivateMessage()
throws Exception {
BdfList invalidList = new BdfList();
for (int i = 0; i < MAX_ATTACHMENTS_PER_MESSAGE + 1; i++) {
invalidList.add(getAttachmentHeader());
}
testRejectsPrivateMessage(
BdfList.of(PRIVATE_MESSAGE, text, invalidList));
}
@Test
public void testAcceptsMaxLengthAttachmentListForPrivateMessage()
throws Exception {
BdfList attachmentList = new BdfList();
for (int i = 0; i < MAX_ATTACHMENTS_PER_MESSAGE; i++) {
attachmentList.add(getAttachmentHeader());
}
BdfDictionary maxAttachmentsMeta = new BdfDictionary(noAttachmentsMeta);
maxAttachmentsMeta.put(MSG_KEY_ATTACHMENT_HEADERS, attachmentList);
testAcceptsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text,
attachmentList), maxAttachmentsMeta);
}
@Test(expected = InvalidMessageException.class)
public void testRejectsNullAttachmentIdForPrivateMessage()
throws Exception {
BdfList invalidHeader = BdfList.of(null, contentType);
testRejectsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text,
BdfList.of(invalidHeader)));
}
@Test(expected = InvalidMessageException.class)
public void testRejectsNonRawAttachmentIdForPrivateMessage()
throws Exception {
BdfList invalidHeader = BdfList.of(123, contentType);
testRejectsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text,
BdfList.of(invalidHeader)));
}
@Test(expected = InvalidMessageException.class)
public void testRejectsTooShortAttachmentIdForPrivateMessage()
throws Exception {
BdfList invalidHeader =
BdfList.of(getRandomBytes(UniqueId.LENGTH - 1), contentType);
testRejectsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text,
BdfList.of(invalidHeader)));
}
@Test(expected = InvalidMessageException.class)
public void testRejectsTooLongAttachmentIdForPrivateMessage()
throws Exception {
BdfList invalidHeader =
BdfList.of(getRandomBytes(UniqueId.LENGTH + 1), contentType);
testRejectsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text,
BdfList.of(invalidHeader)));
}
@Test(expected = InvalidMessageException.class)
public void testRejectsNullContentTypeForPrivateMessage()
throws Exception {
BdfList invalidHeader = BdfList.of(attachmentId, null);
testRejectsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text,
BdfList.of(invalidHeader)));
}
@Test(expected = InvalidMessageException.class)
public void testRejectsNonStringContentTypeForPrivateMessage()
throws Exception {
BdfList invalidHeader = BdfList.of(attachmentId, 123);
testRejectsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text,
BdfList.of(invalidHeader)));
}
@Test(expected = InvalidMessageException.class)
public void testRejectsTooShortContentTypeForPrivateMessage()
throws Exception {
BdfList invalidHeader = BdfList.of(attachmentId, "");
testRejectsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text,
BdfList.of(invalidHeader)));
}
@Test(expected = InvalidMessageException.class)
public void testRejectsTooLongContentTypeForPrivateMessage()
throws Exception {
BdfList invalidHeader = BdfList.of(attachmentId,
getRandomString(MAX_CONTENT_TYPE_BYTES + 1));
testRejectsPrivateMessage(BdfList.of(PRIVATE_MESSAGE, text,
BdfList.of(invalidHeader)));
}
@Test(expected = InvalidMessageException.class)
public void testRejectsTooShortDescriptorWithoutTrailingDataForAttachment()
throws Exception {
expectCheckTimestamp(now);
expectParseList(BdfList.of(ATTACHMENT));
// Single-element list is interpreted as a legacy private message, so
// EOF is expected
expectReadEof(true);
validator.validateMessage(message, group);
}
@Test(expected = InvalidMessageException.class)
public void testRejectsTooShortDescriptorWithTrailingDataForAttachment()
throws Exception {
expectCheckTimestamp(now);
expectParseList(BdfList.of(ATTACHMENT));
// Single-element list is interpreted as a legacy private message, so
// EOF is expected
expectReadEof(false);
validator.validateMessage(message, group);
}
@Test(expected = InvalidMessageException.class)
public void testRejectsTooLongDescriptorForAttachment() throws Exception {
testRejectsAttachment(BdfList.of(ATTACHMENT, contentType, 123));
}
@Test(expected = InvalidMessageException.class)
public void testRejectsNullContentTypeForAttachment() throws Exception {
testRejectsAttachment(BdfList.of(ATTACHMENT, null));
}
@Test(expected = InvalidMessageException.class)
public void testRejectsNonStringContentTypeForAttachment()
throws Exception {
testRejectsAttachment(BdfList.of(ATTACHMENT, 123));
}
@Test(expected = InvalidMessageException.class)
public void testRejectsTooShortContentTypeForAttachment() throws Exception {
testRejectsAttachment(BdfList.of(ATTACHMENT, ""));
}
@Test(expected = InvalidMessageException.class)
public void testRejectsTooLongContentTypeForAttachment() throws Exception {
String invalidContentType = getRandomString(MAX_CONTENT_TYPE_BYTES + 1);
testRejectsAttachment(BdfList.of(ATTACHMENT, invalidContentType));
}
@Test
public void testAcceptsValidDescriptorForAttachment() throws Exception {
expectCheckTimestamp(now);
expectParseList(BdfList.of(ATTACHMENT, contentType));
expectEncodeMetadata(attachmentMeta);
validator.validateMessage(message, group);
}
@Test(expected = InvalidMessageException.class)
public void testRejectsUnknownMessageType() throws Exception {
expectCheckTimestamp(now);
expectParseList(BdfList.of(ATTACHMENT + 1, contentType));
validator.validateMessage(message, group);
}
private void testRejectsLegacyMessage(BdfList body) throws Exception {
expectCheckTimestamp(now);
expectParseList(body);
expectReadEof(true);
validator.validateMessage(message, group);
}
private void testAcceptsLegacyMessage(BdfList body) throws Exception {
expectCheckTimestamp(now);
expectParseList(body);
expectReadEof(true);
expectEncodeMetadata(legacyMeta);
MessageContext result = validator.validateMessage(message, group);
assertEquals(0, result.getDependencies().size());
}
private void testRejectsPrivateMessage(BdfList body) throws Exception {
expectCheckTimestamp(now);
expectParseList(body);
expectReadEof(true);
validator.validateMessage(message, group);
}
private void testAcceptsPrivateMessage(BdfList body, BdfDictionary meta)
throws Exception {
expectCheckTimestamp(now);
expectParseList(body);
expectReadEof(true);
expectEncodeMetadata(meta);
MessageContext result = validator.validateMessage(message, group);
assertEquals(0, result.getDependencies().size());
}
private void testRejectsAttachment(BdfList descriptor) throws Exception {
expectCheckTimestamp(now);
expectParseList(descriptor);
validator.validateMessage(message, group);
}
private void expectCheckTimestamp(long now) {
context.checking(new Expectations() {{
oneOf(clock).currentTimeMillis();
will(returnValue(now));
}});
}
private void expectParseList(BdfList body) throws Exception {
context.checking(new Expectations() {{
oneOf(bdfReaderFactory).createReader(with(any(InputStream.class)));
will(returnValue(reader));
oneOf(reader).readList();
will(returnValue(body));
}});
}
private void expectReadEof(boolean eof) throws Exception {
context.checking(new Expectations() {{
oneOf(reader).eof();
will(returnValue(eof));
}});
}
private void expectEncodeMetadata(BdfDictionary meta) throws Exception {
context.checking(new Expectations() {{
oneOf(metadataEncoder).encode(meta);
will(returnValue(new Metadata()));
}});
}
private BdfList getAttachmentHeader() {
return BdfList.of(new MessageId(getRandomId()),
getRandomString(MAX_CONTENT_TYPE_BYTES));
} }
} }

View File

@@ -15,9 +15,11 @@ import org.briarproject.bramble.api.sync.event.MessageStateChangedEvent;
import org.briarproject.bramble.test.TestDatabaseConfigModule; import org.briarproject.bramble.test.TestDatabaseConfigModule;
import org.briarproject.bramble.test.TestTransportConnectionReader; import org.briarproject.bramble.test.TestTransportConnectionReader;
import org.briarproject.bramble.test.TestTransportConnectionWriter; import org.briarproject.bramble.test.TestTransportConnectionWriter;
import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.MessagingManager;
import org.briarproject.briar.api.messaging.PrivateMessage; import org.briarproject.briar.api.messaging.PrivateMessage;
import org.briarproject.briar.api.messaging.PrivateMessageFactory; import org.briarproject.briar.api.messaging.PrivateMessageFactory;
import org.briarproject.briar.api.messaging.event.AttachmentReceivedEvent;
import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent; import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent;
import org.briarproject.briar.test.BriarTestCase; import org.briarproject.briar.test.BriarTestCase;
import org.junit.After; import org.junit.After;
@@ -27,9 +29,10 @@ import org.junit.Test;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.InputStream;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import static java.util.Collections.emptyList; import static java.util.Collections.singletonList;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERED; import static org.briarproject.bramble.api.sync.validation.MessageState.DELIVERED;
import static org.briarproject.bramble.test.TestPluginConfigModule.SIMPLEX_TRANSPORT_ID; import static org.briarproject.bramble.test.TestPluginConfigModule.SIMPLEX_TRANSPORT_ID;
@@ -84,10 +87,12 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
read(bob, write(alice, bobId), 2); read(bob, write(alice, bobId), 2);
// Sync Bob's client versions and transport properties // Sync Bob's client versions and transport properties
read(alice, write(bob, aliceId), 2); read(alice, write(bob, aliceId), 2);
// Sync the private message // Sync the private message and the attachment
read(bob, write(alice, bobId), 1); read(bob, write(alice, bobId), 2);
// Bob should have received the private message // Bob should have received the private message
assertTrue(listener.messageAdded); assertTrue(listener.messageAdded);
// Bob should have received the attachment
assertTrue(listener.attachmentAdded);
} }
private ContactId setUp(SimplexMessagingIntegrationTestComponent device, private ContactId setUp(SimplexMessagingIntegrationTestComponent device,
@@ -107,16 +112,20 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
private void sendMessage(SimplexMessagingIntegrationTestComponent device, private void sendMessage(SimplexMessagingIntegrationTestComponent device,
ContactId contactId) throws Exception { ContactId contactId) throws Exception {
// Send Bob a message
MessagingManager messagingManager = device.getMessagingManager(); MessagingManager messagingManager = device.getMessagingManager();
GroupId groupId = messagingManager.getConversationId(contactId); GroupId groupId = messagingManager.getConversationId(contactId);
long timestamp = System.currentTimeMillis();
InputStream in = new ByteArrayInputStream(new byte[] {0, 1, 2, 3});
AttachmentHeader attachmentHeader = messagingManager.addLocalAttachment(
groupId, timestamp, "image/png", in);
PrivateMessageFactory privateMessageFactory = PrivateMessageFactory privateMessageFactory =
device.getPrivateMessageFactory(); device.getPrivateMessageFactory();
PrivateMessage message = privateMessageFactory.createPrivateMessage( PrivateMessage message = privateMessageFactory.createPrivateMessage(
groupId, System.currentTimeMillis(), "Hi!", emptyList()); groupId, timestamp, "Hi!", singletonList(attachmentHeader));
messagingManager.addLocalMessage(message); messagingManager.addLocalMessage(message);
} }
@SuppressWarnings("SameParameterValue")
private void read(SimplexMessagingIntegrationTestComponent device, private void read(SimplexMessagingIntegrationTestComponent device,
byte[] stream, int deliveries) throws Exception { byte[] stream, int deliveries) throws Exception {
// Listen for message deliveries // Listen for message deliveries
@@ -187,10 +196,15 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
private static class PrivateMessageListener implements EventListener { private static class PrivateMessageListener implements EventListener {
private volatile boolean messageAdded = false; private volatile boolean messageAdded = false;
private volatile boolean attachmentAdded = false;
@Override @Override
public void eventOccurred(Event e) { public void eventOccurred(Event e) {
if (e instanceof PrivateMessageReceivedEvent) messageAdded = true; if (e instanceof PrivateMessageReceivedEvent) {
messageAdded = true;
} else if (e instanceof AttachmentReceivedEvent) {
attachmentAdded = true;
}
} }
} }
} }

View File

@@ -169,7 +169,8 @@ abstract class AbstractProtocolEngineTest extends BrambleMockTestCase {
oneOf(messageEncoder).encodeMetadata(type, privateGroupId, oneOf(messageEncoder).encodeMetadata(type, privateGroupId,
message.getTimestamp(), true, true, visible, false, false); message.getTimestamp(), true, true, visible, false, false);
will(returnValue(meta)); will(returnValue(meta));
oneOf(clientHelper).addLocalMessage(txn, message, meta, true); oneOf(clientHelper).addLocalMessage(txn, message, meta, true,
false);
}}); }});
} }

View File

@@ -3,6 +3,7 @@ package org.briarproject.briar.headless
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import org.briarproject.bramble.api.FeatureFlags
import org.briarproject.bramble.api.battery.BatteryManager import org.briarproject.bramble.api.battery.BatteryManager
import org.briarproject.bramble.api.db.DatabaseConfig import org.briarproject.bramble.api.db.DatabaseConfig
import org.briarproject.bramble.api.event.EventBus import org.briarproject.bramble.api.event.EventBus
@@ -94,4 +95,9 @@ internal class HeadlessModule(private val appDir: File) {
@Singleton @Singleton
internal fun provideObjectMapper() = ObjectMapper() internal fun provideObjectMapper() = ObjectMapper()
@Provides
internal fun provideFeatureFlags() = object : FeatureFlags {
override fun shouldEnableImageAttachments() = false
override fun shouldEnableRemoteContacts() = true
}
} }

View File

@@ -20,7 +20,7 @@ internal class OutputEvent(val name: String, val data: JsonDict) {
val type = "event" val type = "event"
} }
internal fun ConversationMessageReceivedEvent<*>.output(text: String): JsonDict { internal fun ConversationMessageReceivedEvent<*>.output(text: String?): JsonDict {
check(messageHeader is PrivateMessageHeader) check(messageHeader is PrivateMessageHeader)
return (messageHeader as PrivateMessageHeader).output(contactId, text) return (messageHeader as PrivateMessageHeader).output(contactId, text)
} }

View File

@@ -67,16 +67,16 @@ constructor(
override fun write(ctx: Context): Context { override fun write(ctx: Context): Context {
val contact = getContact(ctx) val contact = getContact(ctx)
val message = ctx.getFromJson(objectMapper, "text") val text = ctx.getFromJson(objectMapper, "text")
if (utf8IsTooLong(message, MAX_PRIVATE_MESSAGE_TEXT_LENGTH)) if (utf8IsTooLong(text, MAX_PRIVATE_MESSAGE_TEXT_LENGTH))
throw BadRequestResponse("Message text is too long") throw BadRequestResponse("Message text is too long")
val group = messagingManager.getContactGroup(contact) val group = messagingManager.getContactGroup(contact)
val now = clock.currentTimeMillis() val now = clock.currentTimeMillis()
val m = privateMessageFactory.createPrivateMessage(group.id, now, message, emptyList()) val m = privateMessageFactory.createLegacyPrivateMessage(group.id, now, text)
messagingManager.addLocalMessage(m) messagingManager.addLocalMessage(m)
return ctx.json(m.output(contact.id, message)) return ctx.json(m.output(contact.id, text))
} }
override fun eventOccurred(e: Event) { override fun eventOccurred(e: Event) {

View File

@@ -3,6 +3,7 @@ package org.briarproject.briar.headless
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import org.briarproject.bramble.api.FeatureFlags
import org.briarproject.bramble.api.db.DatabaseConfig import org.briarproject.bramble.api.db.DatabaseConfig
import org.briarproject.bramble.api.plugin.PluginConfig import org.briarproject.bramble.api.plugin.PluginConfig
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory
@@ -61,4 +62,9 @@ internal class HeadlessTestModule(private val appDir: File) {
@Singleton @Singleton
internal fun provideObjectMapper() = ObjectMapper() internal fun provideObjectMapper() = ObjectMapper()
@Provides
internal fun provideFeatureFlags() = object : FeatureFlags {
override fun shouldEnableImageAttachments() = false
override fun shouldEnableRemoteContacts() = true
}
} }

View File

@@ -115,11 +115,10 @@ internal class MessagingControllerImplTest : ControllerTest() {
every { messagingManager.getContactGroup(contact) } returns group every { messagingManager.getContactGroup(contact) } returns group
every { clock.currentTimeMillis() } returns timestamp every { clock.currentTimeMillis() } returns timestamp
every { every {
privateMessageFactory.createPrivateMessage( privateMessageFactory.createLegacyPrivateMessage(
group.id, group.id,
timestamp, timestamp,
text, text
emptyList()
) )
} returns privateMessage } returns privateMessage
every { messagingManager.addLocalMessage(privateMessage) } just runs every { messagingManager.addLocalMessage(privateMessage) } just runs
@@ -169,7 +168,12 @@ internal class MessagingControllerImplTest : ControllerTest() {
val event = PrivateMessageReceivedEvent(header, contact.id) val event = PrivateMessageReceivedEvent(header, contact.id)
every { messagingManager.getMessageText(message.id) } returns text every { messagingManager.getMessageText(message.id) } returns text
every { webSocketController.sendEvent(EVENT_CONVERSATION_MESSAGE, event.output(text)) } just runs every {
webSocketController.sendEvent(
EVENT_CONVERSATION_MESSAGE,
event.output(text)
)
} just runs
controller.eventOccurred(event) controller.eventOccurred(event)
} }