Separate the sync layer from its clients. #112

This commit is contained in:
akwizgran
2015-12-21 14:36:24 +00:00
parent f5f572139a
commit 5355951466
117 changed files with 3160 additions and 3465 deletions

View File

@@ -2,88 +2,206 @@ package org.briarproject.messaging;
import com.google.inject.Inject;
import org.briarproject.api.FormatException;
import org.briarproject.api.UniqueId;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.SecretKey;
import org.briarproject.api.data.BdfDictionary;
import org.briarproject.api.data.BdfReader;
import org.briarproject.api.data.BdfReaderFactory;
import org.briarproject.api.data.BdfWriter;
import org.briarproject.api.data.BdfWriterFactory;
import org.briarproject.api.data.MetadataEncoder;
import org.briarproject.api.data.MetadataParser;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Metadata;
import org.briarproject.api.db.NoSuchContactException;
import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.messaging.MessagingManager;
import org.briarproject.api.messaging.PrivateConversation;
import org.briarproject.api.messaging.PrivateMessage;
import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupFactory;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageHeader;
import org.briarproject.api.sync.MessageId;
import org.briarproject.api.sync.MessageStatus;
import org.briarproject.util.StringUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import static java.util.logging.Level.WARNING;
import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH;
import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
// Temporary facade during sync protocol refactoring
class MessagingManagerImpl implements MessagingManager {
static final ClientId CLIENT_ID = new ClientId(StringUtils.fromHexString(
"6bcdc006c0910b0f44e40644c3b31f1a"
+ "8bf9a6d6021d40d219c86b731b903070"));
private static final Logger LOG =
Logger.getLogger(MessagingManagerImpl.class.getName());
private final DatabaseComponent db;
private final CryptoComponent crypto;
private final GroupFactory groupFactory;
private final BdfReaderFactory bdfReaderFactory;
private final BdfWriterFactory bdfWriterFactory;
private final MetadataEncoder metadataEncoder;
private final MetadataParser metadataParser;
@Inject
MessagingManagerImpl(DatabaseComponent db, CryptoComponent crypto,
GroupFactory groupFactory) {
MessagingManagerImpl(DatabaseComponent db, GroupFactory groupFactory,
BdfReaderFactory bdfReaderFactory,
BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder,
MetadataParser metadataParser) {
this.db = db;
this.crypto = crypto;
this.groupFactory = groupFactory;
this.bdfReaderFactory = bdfReaderFactory;
this.bdfWriterFactory = bdfWriterFactory;
this.metadataEncoder = metadataEncoder;
this.metadataParser = metadataParser;
}
@Override
public void addContact(ContactId c, SecretKey master) throws DbException {
byte[] salt = crypto.deriveGroupSalt(master);
Group inbox = groupFactory.createGroup("Inbox", salt);
db.addGroup(inbox);
db.setInboxGroup(c, inbox);
public ClientId getClientId() {
return CLIENT_ID;
}
@Override
public void addLocalMessage(Message m) throws DbException {
db.addLocalMessage(m);
public void addContact(ContactId c) throws DbException {
// Create the conversation group
Group conversation = createConversationGroup(db.getContact(c));
// Subscribe to the group and share it with the contact
db.addGroup(conversation);
db.addGroup(c, conversation);
db.setVisibility(conversation.getId(), Collections.singletonList(c));
}
private Group createConversationGroup(Contact c) {
AuthorId local = c.getLocalAuthorId();
AuthorId remote = c.getAuthor().getId();
byte[] descriptor = createGroupDescriptor(local, remote);
return groupFactory.createGroup(CLIENT_ID, descriptor);
}
private byte[] createGroupDescriptor(AuthorId local, AuthorId remote) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
BdfWriter w = bdfWriterFactory.createWriter(out);
try {
w.writeListStart();
if (UniqueId.IdComparator.INSTANCE.compare(local, remote) < 0) {
w.writeRaw(local.getBytes());
w.writeRaw(remote.getBytes());
} else {
w.writeRaw(remote.getBytes());
w.writeRaw(local.getBytes());
}
w.writeListEnd();
} catch (IOException e) {
// Shouldn't happen with ByteArrayOutputStream
throw new RuntimeException(e);
}
return out.toByteArray();
}
@Override
public PrivateConversation getConversation(GroupId g) throws DbException {
return new PrivateConversationImpl(db.getGroup(g));
public void addLocalMessage(PrivateMessage m) throws DbException {
BdfDictionary d = new BdfDictionary();
d.put("timestamp", m.getMessage().getTimestamp());
if (m.getParent() != null) d.put("parent", m.getParent().getBytes());
d.put("contentType", m.getContentType());
d.put("local", true);
d.put("read", true);
try {
Metadata meta = metadataEncoder.encode(d);
db.addLocalMessage(m.getMessage(), CLIENT_ID, meta);
} catch (FormatException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
@Override
public ContactId getContactId(GroupId g) throws DbException {
// TODO: Make this more efficient
for (Contact c : db.getContacts()) {
Group conversation = createConversationGroup(c);
if (conversation.getId().equals(g)) return c.getId();
}
throw new NoSuchContactException();
}
@Override
public GroupId getConversationId(ContactId c) throws DbException {
return db.getInboxGroupId(c);
// TODO: Make this more efficient
return createConversationGroup(db.getContact(c)).getId();
}
@Override
public Collection<PrivateMessageHeader> getMessageHeaders(ContactId c)
throws DbException {
Collection<MessageHeader> headers = db.getInboxMessageHeaders(c);
List<PrivateMessageHeader> privateHeaders =
new ArrayList<PrivateMessageHeader>(headers.size());
for (MessageHeader m : headers)
privateHeaders.add(new PrivateMessageHeaderImpl(m));
return Collections.unmodifiableList(privateHeaders);
GroupId groupId = getConversationId(c);
Map<MessageId, Metadata> metadata = db.getMessageMetadata(groupId);
Collection<MessageStatus> statuses = db.getMessageStatus(c, groupId);
Collection<PrivateMessageHeader> headers =
new ArrayList<PrivateMessageHeader>();
for (MessageStatus s : statuses) {
MessageId id = s.getMessageId();
Metadata m = metadata.get(id);
if (m == null) continue;
try {
BdfDictionary d = metadataParser.parse(m);
long timestamp = d.getInteger("timestamp");
String contentType = d.getString("contentType");
boolean local = d.getBoolean("local");
boolean read = d.getBoolean("read");
headers.add(new PrivateMessageHeader(id, timestamp, contentType,
local, read, s.isSent(), s.isSeen()));
} catch (FormatException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
}
}
return headers;
}
@Override
public byte[] getMessageBody(MessageId m) throws DbException {
return db.getMessageBody(m);
}
@Override
public void setConversation(ContactId c, PrivateConversation p)
throws DbException {
db.setInboxGroup(c, ((PrivateConversationImpl) p).getGroup());
byte[] raw = db.getRawMessage(m);
ByteArrayInputStream in = new ByteArrayInputStream(raw,
MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
BdfReader r = bdfReaderFactory.createReader(in);
try {
// Extract the private message body
r.readListStart();
if (r.hasRaw()) r.skipRaw(); // Parent ID
else r.skipNull(); // No parent
r.skipString(); // Content type
return r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH);
} catch (FormatException e) {
// Not a valid private message
throw new IllegalArgumentException();
} catch (IOException e) {
// Shouldn't happen with ByteArrayInputStream
throw new RuntimeException(e);
}
}
@Override
public void setReadFlag(MessageId m, boolean read) throws DbException {
db.setReadFlag(m, read);
BdfDictionary d = new BdfDictionary();
d.put("read", read);
try {
db.mergeMessageMetadata(m, metadataEncoder.encode(d));
} catch (FormatException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -1,9 +1,17 @@
package org.briarproject.messaging;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import org.briarproject.api.data.BdfReaderFactory;
import org.briarproject.api.data.MetadataEncoder;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.messaging.MessagingManager;
import org.briarproject.api.messaging.PrivateMessageFactory;
import org.briarproject.api.sync.ValidationManager;
import org.briarproject.api.system.Clock;
import javax.inject.Singleton;
public class MessagingModule extends AbstractModule {
@@ -12,4 +20,15 @@ public class MessagingModule extends AbstractModule {
bind(MessagingManager.class).to(MessagingManagerImpl.class);
bind(PrivateMessageFactory.class).to(PrivateMessageFactoryImpl.class);
}
@Provides @Singleton
PrivateMessageValidator getValidator(LifecycleManager lifecycleManager,
ValidationManager validationManager,
BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder,
Clock clock) {
PrivateMessageValidator validator = new PrivateMessageValidator(
validationManager, bdfReaderFactory, metadataEncoder, clock);
lifecycleManager.register(validator);
return validator;
}
}

View File

@@ -1,35 +0,0 @@
package org.briarproject.messaging;
import org.briarproject.api.messaging.PrivateConversation;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId;
// Temporary facade during sync protocol refactoring
class PrivateConversationImpl implements PrivateConversation {
private final Group group;
PrivateConversationImpl(Group group) {
this.group = group;
}
@Override
public GroupId getId() {
return group.getId();
}
Group getGroup() {
return group;
}
@Override
public int hashCode() {
return group.hashCode();
}
@Override
public boolean equals(Object o) {
return o instanceof PrivateConversationImpl
&& group.equals(((PrivateConversationImpl) o).group);
}
}

View File

@@ -1,33 +1,56 @@
package org.briarproject.messaging;
import com.google.inject.Inject;
import org.briarproject.api.messaging.PrivateConversation;
import org.briarproject.api.data.BdfWriter;
import org.briarproject.api.data.BdfWriterFactory;
import org.briarproject.api.messaging.PrivateMessage;
import org.briarproject.api.messaging.PrivateMessageFactory;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageFactory;
import org.briarproject.api.sync.MessageId;
import org.briarproject.util.StringUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
// Temporary facade during sync protocol refactoring
import javax.inject.Inject;
import static org.briarproject.api.messaging.MessagingConstants.MAX_CONTENT_TYPE_LENGTH;
import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH;
class PrivateMessageFactoryImpl implements PrivateMessageFactory {
private final MessageFactory messageFactory;
private final BdfWriterFactory bdfWriterFactory;
@Inject
PrivateMessageFactoryImpl(MessageFactory messageFactory) {
PrivateMessageFactoryImpl(MessageFactory messageFactory,
BdfWriterFactory bdfWriterFactory) {
this.messageFactory = messageFactory;
this.bdfWriterFactory = bdfWriterFactory;
}
@Override
public Message createPrivateMessage(MessageId parent,
PrivateConversation conversation, String contentType,
long timestamp, byte[] body)
public PrivateMessage createPrivateMessage(GroupId groupId, long timestamp,
MessageId parent, String contentType, byte[] body)
throws IOException, GeneralSecurityException {
return messageFactory.createAnonymousMessage(parent,
((PrivateConversationImpl) conversation).getGroup(),
contentType, timestamp, body);
// Validate the arguments
if (StringUtils.toUtf8(contentType).length > MAX_CONTENT_TYPE_LENGTH)
throw new IllegalArgumentException();
if (body.length > MAX_PRIVATE_MESSAGE_BODY_LENGTH)
throw new IllegalArgumentException();
// Serialise the message
ByteArrayOutputStream out = new ByteArrayOutputStream();
BdfWriter w = bdfWriterFactory.createWriter(out);
w.writeListStart();
if (parent == null) w.writeNull();
else w.writeRaw(parent.getBytes());
w.writeString(contentType);
w.writeRaw(body);
w.writeListEnd();
Message m = messageFactory.createMessage(groupId, timestamp,
out.toByteArray());
return new PrivateMessage(m, parent, contentType);
}
}

View File

@@ -1,60 +0,0 @@
package org.briarproject.messaging;
import org.briarproject.api.identity.Author;
import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.sync.MessageHeader;
import org.briarproject.api.sync.MessageId;
// Temporary facade during sync protocol refactoring
public class PrivateMessageHeaderImpl implements PrivateMessageHeader {
private final MessageHeader messageHeader;
PrivateMessageHeaderImpl(MessageHeader messageHeader) {
this.messageHeader = messageHeader;
}
@Override
public MessageId getId() {
return messageHeader.getId();
}
@Override
public Author getAuthor() {
return messageHeader.getAuthor();
}
@Override
public String getContentType() {
return messageHeader.getContentType();
}
@Override
public long getTimestamp() {
return messageHeader.getTimestamp();
}
@Override
public boolean isLocal() {
return messageHeader.isLocal();
}
@Override
public boolean isRead() {
return messageHeader.isRead();
}
@Override
public Status getStatus() {
switch (messageHeader.getStatus()) {
case STORED:
return Status.STORED;
case SENT:
return Status.SENT;
case DELIVERED:
return Status.DELIVERED;
default:
throw new IllegalStateException();
}
}
}

View File

@@ -0,0 +1,103 @@
package org.briarproject.messaging;
import org.briarproject.api.FormatException;
import org.briarproject.api.UniqueId;
import org.briarproject.api.data.BdfDictionary;
import org.briarproject.api.data.BdfReader;
import org.briarproject.api.data.BdfReaderFactory;
import org.briarproject.api.data.MetadataEncoder;
import org.briarproject.api.db.Metadata;
import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId;
import org.briarproject.api.sync.MessageValidator;
import org.briarproject.api.sync.ValidationManager;
import org.briarproject.api.system.Clock;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.logging.Logger;
import javax.inject.Inject;
import static org.briarproject.api.messaging.MessagingConstants.MAX_CONTENT_TYPE_LENGTH;
import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH;
import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
import static org.briarproject.messaging.MessagingManagerImpl.CLIENT_ID;
class PrivateMessageValidator implements MessageValidator {
private static final Logger LOG =
Logger.getLogger(PrivateMessageValidator.class.getName());
private final ValidationManager validationManager;
private final BdfReaderFactory bdfReaderFactory;
private final MetadataEncoder metadataEncoder;
private final Clock clock;
@Inject
PrivateMessageValidator(ValidationManager validationManager,
BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder,
Clock clock) {
this.validationManager = validationManager;
this.bdfReaderFactory = bdfReaderFactory;
this.metadataEncoder = metadataEncoder;
this.clock = clock;
}
@Override
public boolean start() {
validationManager.setMessageValidator(CLIENT_ID, this);
return true;
}
@Override
public boolean stop() {
return true;
}
@Override
public Metadata validateMessage(Message m) {
// Reject the message if it's too far in the future
long now = clock.currentTimeMillis();
if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) {
LOG.info("Timestamp is too far in the future");
return null;
}
try {
// Parse the message body
byte[] raw = m.getRaw();
ByteArrayInputStream in = new ByteArrayInputStream(raw,
MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
BdfReader r = bdfReaderFactory.createReader(in);
MessageId parent = null;
String contentType;
r.readListStart();
// Read the parent ID, if any
if (r.hasRaw()) {
byte[] id = r.readRaw(UniqueId.LENGTH);
if (id.length < UniqueId.LENGTH) throw new FormatException();
parent = new MessageId(id);
} else {
r.readNull();
}
// Read the content type
contentType = r.readString(MAX_CONTENT_TYPE_LENGTH);
// Read the private message body
r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH);
r.readListEnd();
if (!r.eof()) throw new FormatException();
// Return the metadata
BdfDictionary d = new BdfDictionary();
d.put("timestamp", m.getTimestamp());
if (parent != null) d.put("parent", parent.getBytes());
d.put("contentType", contentType);
d.put("local", false);
d.put("read", false);
return metadataEncoder.encode(d);
} catch (IOException e) {
LOG.info("Invalid private message");
return null;
}
}
}