Increased the maximum packet size to 1 MiB.

This should provide acceptable memory usage and database locking
granularity, while making subscription and transport updates large
enough for the incremental update issue to be kicked into the long
grass.

Removed awareness of the serialisation format from the protocol
component wherever possible, and added tests to ensure that the
constants defined in the protocol package's API are compatible with
the serialisation format.
This commit is contained in:
akwizgran
2011-09-07 13:51:30 +01:00
parent 1ac1609dc2
commit 331e7e0547
18 changed files with 258 additions and 42 deletions

View File

@@ -5,6 +5,9 @@ import java.util.Collection;
/** A packet acknowledging receipt of one or more batches. */
public interface Ack {
/** The maximum number of batch IDs per ack. */
static final int MAX_IDS_PER_ACK = 29959;
/** Returns the IDs of the acknowledged batches. */
Collection<BatchId> getBatchIds();
}

View File

@@ -11,9 +11,6 @@ public interface Author extends Writable {
/** The maximum length of an author's public key in bytes. */
static final int MAX_PUBLIC_KEY_LENGTH = 100;
/** The maximum length of a serialised author in bytes. */
static final int MAX_LENGTH = MAX_NAME_LENGTH + MAX_PUBLIC_KEY_LENGTH + 5;
/** Returns the author's unique identifier. */
AuthorId getId();

View File

@@ -11,9 +11,6 @@ public interface Group extends Writable {
/** The maximum length of a group's public key in bytes. */
static final int MAX_PUBLIC_KEY_LENGTH = 100;
/** The maximum length of a serialised group in bytes. */
static final int MAX_LENGTH = MAX_NAME_LENGTH + MAX_PUBLIC_KEY_LENGTH + 5;
/** Returns the group's unique identifier. */
GroupId getId();

View File

@@ -3,22 +3,16 @@ package net.sf.briar.api.protocol;
public interface Message {
/**
* The maximum length of a serialised message in bytes. To allow for future
* changes in the batch format, this is smaller than the amount of data
* that can fit in a batch using the current format.
* The maximum length of a message body in bytes. To allow for future
* changes in the protocol, this is smaller than the maximum packet length
* even when all the message's other fields have their maximum lengths.
*/
static final int MAX_LENGTH = ProtocolConstants.MAX_PACKET_LENGTH - 1024;
static final int MAX_BODY_LENGTH =
ProtocolConstants.MAX_PACKET_LENGTH - 1024;
/** The maximum length of a signature in bytes. */
static final int MAX_SIGNATURE_LENGTH = 100;
/**
* The maximum length of a message body in bytes. To allow for future
* changes in the message format, this is smaller than the amount of data
* that can fit in a message using the current format.
*/
static final int MAX_BODY_LENGTH = MAX_LENGTH - 1024;
/** Returns the message's unique identifier. */
MessageId getId();

View File

@@ -5,6 +5,9 @@ import java.util.Collection;
/** A packet offering the recipient some messages. */
public interface Offer {
/** The maximum number of message IDs per offer. */
static final int MAX_IDS_PER_OFFER = 29959;
/** Returns the offer's unique identifier. */
OfferId getId();

View File

@@ -1,14 +1,12 @@
package net.sf.briar.api.protocol;
import net.sf.briar.api.transport.TransportConstants;
public interface ProtocolConstants {
/**
* The maximum length of a serialised packet in bytes. To allow for future
* changes in the frame format, this is smaller than the amount of data
* that can fit in a frame using the current format.
* The maximum length of a serialised packet in bytes. Since the protocol
* does not aim for low latency, the two main constraints here are the
* amount of memory used for parsing packets and the granularity of the
* database transactions for generating and receiving packets.
*/
static final int MAX_PACKET_LENGTH =
TransportConstants.MAX_FRAME_LENGTH - 1024;
static final int MAX_PACKET_LENGTH = 1024 * 1024; // 1 MiB
}

View File

@@ -5,6 +5,9 @@ import java.util.Map;
/** A packet updating the sender's subscriptions. */
public interface SubscriptionUpdate {
/** The maximum number of subscriptions per update. */
static final int MAX_SUBS_PER_UPDATE = 6000;
/** Returns the subscriptions contained in the update. */
Map<Group, Long> getSubscriptions();

View File

@@ -5,6 +5,18 @@ import java.util.Map;
/** A packet updating the sender's transport properties. */
public interface TransportUpdate {
/** The maximum length of a plugin's name in UTF-8 bytes. */
static final int MAX_NAME_LENGTH = 50;
/** The maximum length of a property's key or value in UTF-8 bytes. */
static final int MAX_KEY_OR_VALUE_LENGTH = 100;
/** The maximum number of properties per plugin. */
static final int MAX_PROPERTIES_PER_PLUGIN = 100;
/** The maximum number of plugins per update. */
static final int MAX_PLUGINS_PER_UPDATE = 50;
/** Returns the transport properties contained in the update. */
Map<String, Map<String, String>> getTransports();

View File

@@ -3,6 +3,7 @@ package net.sf.briar.protocol;
import java.io.IOException;
import java.util.Collection;
import net.sf.briar.api.FormatException;
import net.sf.briar.api.protocol.Ack;
import net.sf.briar.api.protocol.BatchId;
import net.sf.briar.api.protocol.ProtocolConstants;
@@ -30,6 +31,7 @@ class AckReader implements ObjectReader<Ack> {
r.readUserDefinedTag(Tags.ACK);
r.addObjectReader(Tags.BATCH_ID, batchIdReader);
Collection<BatchId> batches = r.readList(BatchId.class);
if(batches.size() > Ack.MAX_IDS_PER_ACK) throw new FormatException();
r.removeObjectReader(Tags.BATCH_ID);
r.removeConsumer(counting);
// Build and return the ack

View File

@@ -78,7 +78,10 @@ class MessageEncoderImpl implements MessageEncoder {
} else {
signature.initSign(authorKey);
signature.update(out.toByteArray());
w.writeBytes(signature.sign());
byte[] sig = signature.sign();
if(sig.length > Message.MAX_SIGNATURE_LENGTH)
throw new IllegalArgumentException();
w.writeBytes(sig);
}
// Sign the message with the group's private key, if there is one
if(groupKey == null) {
@@ -86,7 +89,10 @@ class MessageEncoderImpl implements MessageEncoder {
} else {
signature.initSign(groupKey);
signature.update(out.toByteArray());
w.writeBytes(signature.sign());
byte[] sig = signature.sign();
if(sig.length > Message.MAX_SIGNATURE_LENGTH)
throw new IllegalArgumentException();
w.writeBytes(sig);
}
// Hash the message, including the signatures, to get the message ID
byte[] raw = out.toByteArray();

View File

@@ -14,6 +14,7 @@ import net.sf.briar.api.protocol.AuthorId;
import net.sf.briar.api.protocol.Group;
import net.sf.briar.api.protocol.Message;
import net.sf.briar.api.protocol.MessageId;
import net.sf.briar.api.protocol.ProtocolConstants;
import net.sf.briar.api.protocol.Tags;
import net.sf.briar.api.serial.ObjectReader;
import net.sf.briar.api.serial.Reader;
@@ -41,7 +42,8 @@ class MessageReader implements ObjectReader<Message> {
public Message readObject(Reader r) throws IOException {
CopyingConsumer copying = new CopyingConsumer();
CountingConsumer counting = new CountingConsumer(Message.MAX_LENGTH);
CountingConsumer counting =
new CountingConsumer(ProtocolConstants.MAX_PACKET_LENGTH);
r.addConsumer(copying);
r.addConsumer(counting);
// Read the initial tag
@@ -64,7 +66,7 @@ class MessageReader implements ObjectReader<Message> {
long timestamp = r.readInt64();
if(timestamp < 0L) throw new FormatException();
// Skip the message body
r.readBytes(Message.MAX_LENGTH);
r.readBytes(Message.MAX_BODY_LENGTH);
// Record the length of the data covered by the author's signature
int signedByAuthor = (int) counting.getCount();
// Read the author's signature, if there is one

View File

@@ -4,6 +4,7 @@ import java.io.IOException;
import java.security.MessageDigest;
import java.util.Collection;
import net.sf.briar.api.FormatException;
import net.sf.briar.api.crypto.CryptoComponent;
import net.sf.briar.api.protocol.MessageId;
import net.sf.briar.api.protocol.Offer;
@@ -39,6 +40,8 @@ class OfferReader implements ObjectReader<Offer> {
r.readUserDefinedTag(Tags.OFFER);
r.addObjectReader(Tags.MESSAGE_ID, messageIdReader);
Collection<MessageId> messages = r.readList(MessageId.class);
if(messages.size() > Offer.MAX_IDS_PER_OFFER)
throw new FormatException();
r.removeObjectReader(Tags.MESSAGE_ID);
r.removeConsumer(digesting);
r.removeConsumer(counting);

View File

@@ -5,6 +5,7 @@ import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import net.sf.briar.api.FormatException;
import net.sf.briar.api.protocol.ProtocolConstants;
import net.sf.briar.api.protocol.Tags;
import net.sf.briar.api.protocol.TransportUpdate;
@@ -34,9 +35,14 @@ class TransportReader implements ObjectReader<TransportUpdate> {
List<TransportProperties> l = r.readList(TransportProperties.class);
r.resetMaxStringLength();
r.removeObjectReader(Tags.TRANSPORT_PROPERTIES);
if(l.size() > TransportUpdate.MAX_PLUGINS_PER_UPDATE)
throw new FormatException();
Map<String, Map<String, String>> transports =
new TreeMap<String, Map<String, String>>();
for(TransportProperties t : l) transports.put(t.name, t.properties);
for(TransportProperties t : l) {
if(transports.put(t.name, t.properties) != null)
throw new FormatException(); // Duplicate plugin name
}
long timestamp = r.readInt64();
r.removeConsumer(counting);
// Build and return the transport update
@@ -59,9 +65,13 @@ class TransportReader implements ObjectReader<TransportUpdate> {
public TransportProperties readObject(Reader r) throws IOException {
r.readUserDefinedTag(Tags.TRANSPORT_PROPERTIES);
String name = r.readString();
String name = r.readString(TransportUpdate.MAX_NAME_LENGTH);
r.setMaxStringLength(TransportUpdate.MAX_KEY_OR_VALUE_LENGTH);
Map<String, String> properties =
r.readMap(String.class, String.class);
r.resetMaxStringLength();
if(properties.size() > TransportUpdate.MAX_PROPERTIES_PER_PLUGIN)
throw new FormatException();
return new TransportProperties(name, properties);
}
}

View File

@@ -3,8 +3,8 @@ package net.sf.briar.protocol.writers;
import java.io.IOException;
import java.io.OutputStream;
import net.sf.briar.api.protocol.Ack;
import net.sf.briar.api.protocol.BatchId;
import net.sf.briar.api.protocol.ProtocolConstants;
import net.sf.briar.api.protocol.Tags;
import net.sf.briar.api.protocol.writers.AckWriter;
import net.sf.briar.api.serial.Writer;
@@ -16,6 +16,7 @@ class AckWriterImpl implements AckWriter {
private final Writer w;
private boolean started = false;
private int idsWritten = 0;
AckWriterImpl(OutputStream out, WriterFactory writerFactory) {
this.out = out;
@@ -28,12 +29,9 @@ class AckWriterImpl implements AckWriter {
w.writeListStart();
started = true;
}
int capacity = ProtocolConstants.MAX_PACKET_LENGTH
- (int) w.getBytesWritten() - 1;
// Allow one byte for the BATCH_ID tag, one byte for the BYTES tag and
// one byte for the length as a uint7
if(capacity < BatchId.LENGTH + 3) return false;
if(idsWritten >= Ack.MAX_IDS_PER_ACK) return false;
b.writeTo(w);
idsWritten++;
return true;
}

View File

@@ -40,6 +40,7 @@ class BatchWriterImpl implements BatchWriter {
w.writeListStart();
started = true;
}
// Allow one byte for the list end tag
int capacity = ProtocolConstants.MAX_PACKET_LENGTH
- (int) w.getBytesWritten() - 1;
if(capacity < message.length) return false;

View File

@@ -6,8 +6,8 @@ import java.security.DigestOutputStream;
import java.security.MessageDigest;
import net.sf.briar.api.protocol.MessageId;
import net.sf.briar.api.protocol.Offer;
import net.sf.briar.api.protocol.OfferId;
import net.sf.briar.api.protocol.ProtocolConstants;
import net.sf.briar.api.protocol.Tags;
import net.sf.briar.api.protocol.writers.OfferWriter;
import net.sf.briar.api.serial.Writer;
@@ -20,6 +20,7 @@ class OfferWriterImpl implements OfferWriter {
private final MessageDigest messageDigest;
private boolean started = false;
private int idsWritten = 0;
OfferWriterImpl(OutputStream out, WriterFactory writerFactory,
MessageDigest messageDigest) {
@@ -35,12 +36,9 @@ class OfferWriterImpl implements OfferWriter {
w.writeListStart();
started = true;
}
int capacity = ProtocolConstants.MAX_PACKET_LENGTH
- (int) w.getBytesWritten() - 1;
// Allow one byte for the MESSAGE_ID tag, one byte for the BYTES tag
// and one byte for the length as a uint7
if(capacity < MessageId.LENGTH + 3) return false;
if(idsWritten >= Offer.MAX_IDS_PER_OFFER) return false;
m.writeTo(w);
idsWritten++;
return true;
}

View File

@@ -31,6 +31,7 @@
<test name='net.sf.briar.protocol.ConsumersTest'/>
<test name='net.sf.briar.protocol.ProtocolReadWriteTest'/>
<test name='net.sf.briar.protocol.RequestReaderTest'/>
<test name='net.sf.briar.protocol.writers.ConstantsTest'/>
<test name='net.sf.briar.protocol.writers.RequestWriterImplTest'/>
<test name='net.sf.briar.serial.ReaderImplTest'/>
<test name='net.sf.briar.serial.WriterImplTest'/>

View File

@@ -0,0 +1,188 @@
package net.sf.briar.protocol.writers;
import java.io.ByteArrayOutputStream;
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.Map;
import junit.framework.TestCase;
import net.sf.briar.TestUtils;
import net.sf.briar.api.crypto.CryptoComponent;
import net.sf.briar.api.protocol.Ack;
import net.sf.briar.api.protocol.Author;
import net.sf.briar.api.protocol.AuthorFactory;
import net.sf.briar.api.protocol.BatchId;
import net.sf.briar.api.protocol.Group;
import net.sf.briar.api.protocol.GroupFactory;
import net.sf.briar.api.protocol.Message;
import net.sf.briar.api.protocol.MessageEncoder;
import net.sf.briar.api.protocol.MessageId;
import net.sf.briar.api.protocol.Offer;
import net.sf.briar.api.protocol.ProtocolConstants;
import net.sf.briar.api.protocol.SubscriptionUpdate;
import net.sf.briar.api.protocol.TransportUpdate;
import net.sf.briar.api.protocol.UniqueId;
import net.sf.briar.api.protocol.writers.AckWriter;
import net.sf.briar.api.protocol.writers.BatchWriter;
import net.sf.briar.api.protocol.writers.OfferWriter;
import net.sf.briar.api.protocol.writers.SubscriptionWriter;
import net.sf.briar.api.protocol.writers.TransportWriter;
import net.sf.briar.api.serial.WriterFactory;
import net.sf.briar.crypto.CryptoModule;
import net.sf.briar.protocol.ProtocolModule;
import net.sf.briar.serial.SerialModule;
import org.junit.Test;
import com.google.inject.Guice;
import com.google.inject.Injector;
public class ConstantsTest extends TestCase {
private final WriterFactory writerFactory;
private final CryptoComponent crypto;
private final GroupFactory groupFactory;
private final AuthorFactory authorFactory;
private final MessageEncoder messageEncoder;
public ConstantsTest() throws Exception {
super();
Injector i = Guice.createInjector(new CryptoModule(),
new ProtocolModule(), new SerialModule());
writerFactory = i.getInstance(WriterFactory.class);
crypto = i.getInstance(CryptoComponent.class);
groupFactory = i.getInstance(GroupFactory.class);
authorFactory = i.getInstance(AuthorFactory.class);
messageEncoder = i.getInstance(MessageEncoder.class);
}
@Test
public void testBatchesFitIntoAck() throws Exception {
// Create an ack with the maximum number of batch IDs
ByteArrayOutputStream out = new ByteArrayOutputStream(
ProtocolConstants.MAX_PACKET_LENGTH);
AckWriter a = new AckWriterImpl(out, writerFactory);
for(int i = 0; i < Ack.MAX_IDS_PER_ACK; i++) {
assertTrue(a.writeBatchId(new BatchId(TestUtils.getRandomId())));
}
// Check that no more batch IDs can be written
assertFalse(a.writeBatchId(new BatchId(TestUtils.getRandomId())));
a.finish();
// Check the size of the ack
assertTrue(out.size() > UniqueId.LENGTH * Ack.MAX_IDS_PER_ACK);
assertTrue(out.size() <= ProtocolConstants.MAX_PACKET_LENGTH);
}
@Test
public void testMessageFitsIntoBatch() throws Exception {
// Create a maximum-length group
String groupName = createRandomString(Group.MAX_NAME_LENGTH);
byte[] groupPublic = new byte[Group.MAX_PUBLIC_KEY_LENGTH];
Group group = groupFactory.createGroup(groupName, groupPublic);
// Create a maximum-length author
String authorName = createRandomString(Author.MAX_NAME_LENGTH);
byte[] authorPublic = new byte[Author.MAX_PUBLIC_KEY_LENGTH];
Author author = authorFactory.createAuthor(authorName, authorPublic);
// Create a maximum-length message
PrivateKey groupPrivate = crypto.generateKeyPair().getPrivate();
PrivateKey authorPrivate = crypto.generateKeyPair().getPrivate();
byte[] body = new byte[Message.MAX_BODY_LENGTH];
Message message = messageEncoder.encodeMessage(MessageId.NONE, group,
groupPrivate, author, authorPrivate, body);
// Add the message to a batch
ByteArrayOutputStream out = new ByteArrayOutputStream(
ProtocolConstants.MAX_PACKET_LENGTH);
BatchWriter b = new BatchWriterImpl(out, writerFactory,
crypto.getMessageDigest());
b.writeMessage(message.getBytes());
b.finish();
// Check the size of the serialised batch
assertTrue(out.size() > UniqueId.LENGTH + Group.MAX_NAME_LENGTH +
Group.MAX_PUBLIC_KEY_LENGTH + Author.MAX_NAME_LENGTH +
Author.MAX_PUBLIC_KEY_LENGTH + Message.MAX_BODY_LENGTH);
assertTrue(out.size() <= ProtocolConstants.MAX_PACKET_LENGTH);
}
@Test
public void testMessagesFitIntoOffer() throws Exception {
// Create an offer with the maximum number of message IDs
ByteArrayOutputStream out = new ByteArrayOutputStream(
ProtocolConstants.MAX_PACKET_LENGTH);
OfferWriter o = new OfferWriterImpl(out, writerFactory,
crypto.getMessageDigest());
for(int i = 0; i < Offer.MAX_IDS_PER_OFFER; i++) {
assertTrue(o.writeMessageId(new MessageId(
TestUtils.getRandomId())));
}
// Check that no more message IDs can be written
assertFalse(o.writeMessageId(new MessageId(TestUtils.getRandomId())));
o.finish();
// Check the size of the offer
assertTrue(out.size() > UniqueId.LENGTH * Offer.MAX_IDS_PER_OFFER);
assertTrue(out.size() <= ProtocolConstants.MAX_PACKET_LENGTH);
}
@Test
public void testSubscriptionsFitIntoUpdate() throws Exception {
// Create the maximum number of maximum-length subscriptions
Map<Group, Long> subs =
new HashMap<Group, Long>(SubscriptionUpdate.MAX_SUBS_PER_UPDATE);
byte[] publicKey = new byte[Group.MAX_PUBLIC_KEY_LENGTH];
for(int i = 0; i < SubscriptionUpdate.MAX_SUBS_PER_UPDATE; i++) {
String name = createRandomString(Group.MAX_NAME_LENGTH);
Group group = groupFactory.createGroup(name, publicKey);
subs.put(group, Long.MAX_VALUE);
}
// Add the subscriptions to an update
ByteArrayOutputStream out = new ByteArrayOutputStream(
ProtocolConstants.MAX_PACKET_LENGTH);
SubscriptionWriter s = new SubscriptionWriterImpl(out, writerFactory);
s.writeSubscriptions(subs, Long.MAX_VALUE);
// Check the size of the serialised update
assertTrue(out.size() > SubscriptionUpdate.MAX_SUBS_PER_UPDATE *
(Group.MAX_NAME_LENGTH + Group.MAX_PUBLIC_KEY_LENGTH + 8) + 8);
assertTrue(out.size() <= ProtocolConstants.MAX_PACKET_LENGTH);
}
@Test
public void testTransportsFitIntoUpdate() throws Exception {
// Create the maximum number of plugins, each with the maximum number
// of maximum-length properties
Map<String, Map<String, String>> transports =
new HashMap<String, Map<String, String>>(
TransportUpdate.MAX_PLUGINS_PER_UPDATE);
for(int i = 0; i < TransportUpdate.MAX_PLUGINS_PER_UPDATE; i++) {
String name = createRandomString(TransportUpdate.MAX_NAME_LENGTH);
Map<String, String> properties = new HashMap<String, String>(
TransportUpdate.MAX_PROPERTIES_PER_PLUGIN);
for(int j = 0; j < TransportUpdate.MAX_PROPERTIES_PER_PLUGIN; j++) {
String key = createRandomString(
TransportUpdate.MAX_KEY_OR_VALUE_LENGTH);
String value = createRandomString(
TransportUpdate.MAX_KEY_OR_VALUE_LENGTH);
properties.put(key, value);
}
transports.put(name, properties);
}
// Add the transports to an update
ByteArrayOutputStream out = new ByteArrayOutputStream(
ProtocolConstants.MAX_PACKET_LENGTH);
TransportWriter t = new TransportWriterImpl(out, writerFactory);
t.writeTransports(transports, Long.MAX_VALUE);
// Check the size of the serialised update
assertTrue(out.size() > TransportUpdate.MAX_PLUGINS_PER_UPDATE *
(TransportUpdate.MAX_NAME_LENGTH +
TransportUpdate.MAX_PROPERTIES_PER_PLUGIN *
TransportUpdate.MAX_KEY_OR_VALUE_LENGTH * 2));
assertTrue(out.size() <= ProtocolConstants.MAX_PACKET_LENGTH);
}
private static String createRandomString(int length) {
StringBuilder s = new StringBuilder(length);
for(int i = 0; i < length; i++) {
int digit = (int) (Math.random() * 10);
s.append((char) ('0' + digit));
}
return s.toString();
}
}