From 0bb455d433641eceb878429dcd41909725273010 Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Wed, 30 Nov 2016 17:26:22 +0100 Subject: [PATCH 01/22] Extended encoding basics works for basic subject/body messages, no attachments and no other features supported yet --- core/build.gradle | 1 + .../dissem/bitmessage/entity/Plaintext.java | 88 ++++++-- .../entity/valueobject/Attachment.java | 95 ++++++++ .../entity/valueobject/ExtendedEncoding.java | 203 ++++++++++++++++++ .../entity/ExtendedEncodingTest.java | 39 ++++ .../bitmessage/entity/SerializationTest.java | 60 ++++-- 6 files changed, 457 insertions(+), 29 deletions(-) create mode 100644 core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Attachment.java create mode 100644 core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java create mode 100644 core/src/test/java/ch/dissem/bitmessage/entity/ExtendedEncodingTest.java diff --git a/core/build.gradle b/core/build.gradle index 73f8fdf..1c2d07f 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -25,6 +25,7 @@ artifacts { dependencies { compile 'org.slf4j:slf4j-api:1.7.12' + compile 'org.msgpack:msgpack-core:0.8.11' testCompile 'junit:junit:4.12' testCompile 'org.hamcrest:hamcrest-library:1.3' testCompile 'org.mockito:mockito-core:1.10.19' diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java index 710e041..e08df3d 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java @@ -18,17 +18,23 @@ package ch.dissem.bitmessage.entity; import ch.dissem.bitmessage.entity.payload.Msg; import ch.dissem.bitmessage.entity.payload.Pubkey.Feature; +import ch.dissem.bitmessage.entity.valueobject.Attachment; +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.factory.Factory; -import ch.dissem.bitmessage.utils.*; +import ch.dissem.bitmessage.utils.Decode; +import ch.dissem.bitmessage.utils.Encode; +import ch.dissem.bitmessage.utils.TTL; +import ch.dissem.bitmessage.utils.UnixTime; import java.io.*; import java.nio.ByteBuffer; import java.util.*; -import java.util.Collections; +import static ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED; +import static ch.dissem.bitmessage.entity.Plaintext.Encoding.SIMPLE; import static ch.dissem.bitmessage.utils.Singleton.cryptography; /** @@ -42,6 +48,7 @@ public class Plaintext implements Streamable { private final long encoding; private final byte[] message; private final byte[] ackData; + private ExtendedEncoding extendedData; private ObjectMessage ackMessage; private Object id; private InventoryVector inventoryVector; @@ -143,6 +150,10 @@ public class Plaintext implements Streamable { return labels; } + public Encoding getEncoding() { + return Encoding.fromCode(encoding); + } + public long getStream() { return from.getStream(); } @@ -299,7 +310,13 @@ public class Plaintext implements Streamable { public String getSubject() { Scanner s = new Scanner(new ByteArrayInputStream(message), "UTF-8"); String firstLine = s.nextLine(); - if (encoding == 2) { + if (encoding == EXTENDED.code) { + if (getExtendedData().getMessage() == null) { + return null; + } else { + return extendedData.getMessage().getSubject(); + } + } else if (encoding == SIMPLE.code) { return firstLine.substring("Subject:".length()).trim(); } else if (firstLine.length() > 50) { return firstLine.substring(0, 50).trim() + "..."; @@ -309,14 +326,46 @@ public class Plaintext implements Streamable { } public String getText() { - try { - String text = new String(message, "UTF-8"); - if (encoding == 2) { - return text.substring(text.indexOf("\nBody:") + 6); + if (encoding == EXTENDED.code) { + if (getExtendedData().getMessage() == null) { + return null; + } else { + return extendedData.getMessage().getBody(); } - return text; - } catch (UnsupportedEncodingException e) { - throw new ApplicationException(e); + } else { + try { + String text = new String(message, "UTF-8"); + if (encoding == SIMPLE.code) { + return text.substring(text.indexOf("\nBody:") + 6); + } + return text; + } catch (UnsupportedEncodingException e) { + throw new ApplicationException(e); + } + } + } + + protected ExtendedEncoding getExtendedData() { + if (extendedData == null && encoding == EXTENDED.code) { + // TODO: make sure errors are properly handled + extendedData = ExtendedEncoding.unzip(message); + } + return extendedData; + } + + public List getParents() { + if (getExtendedData() == null || extendedData.getMessage() == null) { + return Collections.emptyList(); + } else { + return extendedData.getMessage().getParents(); + } + } + + public List getFiles() { + if (getExtendedData() == null || extendedData.getMessage() == null) { + return Collections.emptyList(); + } else { + return extendedData.getMessage().getFiles(); } } @@ -386,7 +435,7 @@ public class Plaintext implements Streamable { } public enum Encoding { - IGNORE(0), TRIVIAL(1), SIMPLE(2); + IGNORE(0), TRIVIAL(1), SIMPLE(2), EXTENDED(3); long code; @@ -397,6 +446,15 @@ public class Plaintext implements Streamable { public long getCode() { return code; } + + public static Encoding fromCode(long code) { + for (Encoding e : values()) { + if (e.getCode() == code) { + return e; + } + } + return null; + } } public enum Status { @@ -517,9 +575,15 @@ public class Plaintext implements Streamable { return this; } + public Builder message(ExtendedEncoding message) { + this.encoding = EXTENDED.getCode(); + this.message = message.zip(); + return this; + } + public Builder message(String subject, String message) { try { - this.encoding = Encoding.SIMPLE.getCode(); + this.encoding = SIMPLE.getCode(); this.message = ("Subject:" + subject + '\n' + "Body:" + message).getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new ApplicationException(e); diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Attachment.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Attachment.java new file mode 100644 index 0000000..8ea64de --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Attachment.java @@ -0,0 +1,95 @@ +package ch.dissem.bitmessage.entity.valueobject; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Objects; + +/** + * A "file" attachment as used by extended encoding type messages. Could either be an attachment, + * or used inline to be used by a HTML message, for example. + */ +public class Attachment implements Serializable { + private static final long serialVersionUID = 7319139427666943189L; + + private String name; + private byte[] data; + private String type; + private Disposition disposition; + + public String getName() { + return name; + } + + public byte[] getData() { + return data; + } + + public String getType() { + return type; + } + + public Disposition getDisposition() { + return disposition; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Attachment that = (Attachment) o; + return Objects.equals(name, that.name) && + Arrays.equals(data, that.data) && + Objects.equals(type, that.type) && + disposition == that.disposition; + } + + @Override + public int hashCode() { + return Objects.hash(name, data, type, disposition); + } + + private enum Disposition { + inline, attachment + } + + public static final class Builder { + private String name; + private byte[] data; + private String type; + private Disposition disposition; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder data(byte[] data) { + this.data = data; + return this; + } + + public Builder type(String type) { + this.type = type; + return this; + } + + public Builder inline() { + this.disposition = Disposition.inline; + return this; + } + + public Builder attachment() { + this.disposition = Disposition.attachment; + return this; + } + + public Attachment build() { + Attachment attachment = new Attachment(); + attachment.type = this.type; + attachment.disposition = this.disposition; + attachment.data = this.data; + attachment.name = this.name; + return attachment; + } + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java new file mode 100644 index 0000000..699cd22 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java @@ -0,0 +1,203 @@ +package ch.dissem.bitmessage.entity.valueobject; + +import ch.dissem.bitmessage.exception.ApplicationException; +import org.msgpack.core.MessagePack; +import org.msgpack.core.MessagePacker; +import org.msgpack.core.MessageUnpacker; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.Serializable; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterInputStream; + +/** + * Extended encoding message object. + */ +public class ExtendedEncoding implements Serializable { + private static final long serialVersionUID = 3876871488247305200L; + + private Message message; + + public ExtendedEncoding(Message message) { + this.message = message; + } + + private ExtendedEncoding() { + } + + public Message getMessage() { + return message; + } + + public byte[] zip() { + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + DeflaterOutputStream zipper = new DeflaterOutputStream(out)) { + + MessagePacker packer = MessagePack.newDefaultPacker(zipper); + // FIXME: this should work for trivial cases + if (message != null) { + message.pack(packer); + } + packer.close(); + zipper.close(); + return out.toByteArray(); + } catch (IOException e) { + throw new ApplicationException(e); + } + } + + public static ExtendedEncoding unzip(byte[] zippedData) { + ExtendedEncoding result = new ExtendedEncoding(); + try (InflaterInputStream unzipper = new InflaterInputStream(new ByteArrayInputStream(zippedData))) { + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(unzipper); + int mapSize = unpacker.unpackMapHeader(); + for (int i = 0; i < mapSize; i++) { + String key = unpacker.unpackString(); + switch (key) { + case "": + switch (unpacker.unpackString()) { + case "message": + result.message = new Message(); + break; + } + break; + case "subject": + result.message.subject = unpacker.unpackString(); + break; + case "body": + result.message.body = unpacker.unpackString(); + break; + default: + break; + } + } + } catch (IOException e) { + throw new ApplicationException(e); + } + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExtendedEncoding that = (ExtendedEncoding) o; + return Objects.equals(message, that.message); + } + + @Override + public int hashCode() { + return Objects.hash(message); + } + + public static class Message implements Serializable { + private static final long serialVersionUID = -2724977231484285467L; + + private String subject; + private String body; + private List parents; + private List files; + + private Message() { + parents = Collections.emptyList(); + files = Collections.emptyList(); + } + + private Message(Builder builder) { + subject = builder.subject; + body = builder.body; + parents = Collections.unmodifiableList(builder.parents); + files = Collections.unmodifiableList(builder.files); + } + + public String getSubject() { + return subject; + } + + public String getBody() { + return body; + } + + public List getParents() { + return parents; + } + + public List getFiles() { + return files; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Message message = (Message) o; + return Objects.equals(subject, message.subject) && + Objects.equals(body, message.body) && + Objects.equals(parents, message.parents) && + Objects.equals(files, message.files); + } + + @Override + public int hashCode() { + return Objects.hash(subject, body, parents, files); + } + + public void pack(MessagePacker packer) throws IOException { + packer.packMapHeader(3); + packer.packString(""); + packer.packString("message"); + packer.packString("subject"); + packer.packString(subject); + packer.packString("body"); + packer.packString(body); + } + + public static class Builder { + private String subject; + private String body; + private List parents = new LinkedList<>(); + private List files = new LinkedList<>(); + + private Builder() { + } + + public Builder subject(String subject) { + this.subject = subject; + return this; + } + + public Builder body(String body) { + this.body = body; + return this; + } + + public Builder addParent(InventoryVector iv) { + parents.add(iv); + return this; + } + + public Builder addFile(Attachment file) { + files.add(file); + return this; + } + + public ExtendedEncoding build() { + return new ExtendedEncoding(new Message(this)); + } + } + } + + public static class Builder { + public Message.Builder message() { + return new Message.Builder(); + } + + // TODO: vote (etc.?) + } +} diff --git a/core/src/test/java/ch/dissem/bitmessage/entity/ExtendedEncodingTest.java b/core/src/test/java/ch/dissem/bitmessage/entity/ExtendedEncodingTest.java new file mode 100644 index 0000000..b4a1017 --- /dev/null +++ b/core/src/test/java/ch/dissem/bitmessage/entity/ExtendedEncodingTest.java @@ -0,0 +1,39 @@ +package ch.dissem.bitmessage.entity; + +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; +import ch.dissem.bitmessage.utils.Bytes; +import org.junit.Test; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +/** + * @author Christian Basler + */ +public class ExtendedEncodingTest { + + @Test + public void ensureSimpleMessageIsDecoded() { + ExtendedEncoding extended = ExtendedEncoding.unzip(Bytes.fromHex("78da9d59dd8e1bb715ee359fa097c4b65b498876babb76d0760bd970368eedb68e8daed3c0700c81d25012b333c3e99063ad1c0430d0a728d002bd289077caa59fa4df39244723693705b2f0da339c730ebff3c3f343ffe33f3f94da39b5d4ff9dd97cf3e32f9d10524afe4b3a2b57762dfd4acb453bbf967355c967b2755a1a2f1f0afe791eb85f2a7c374e2ab950ce8fe5dc96b59a7b3933956a36d2e9c6a8c2bc57ded84a2e6c532a10b9d678352b342d48674a53a846e6ca2be9adfcd3d58b2f33f96a05a190748d3d64ddd87726d74e5ebedcf81504417a6eaaa563018d56f4225595cb75633c3df7d191e04c3cab9c574521c4af656d6a69c2ab2cdd927639a959b0102f372f3742ecae6ef7afdb0668c2a229eb4297baf2ac5b2689932d4576f2809f89af01d3ae61d9af57ba921bdbd2f78f1ffee999269a2837ce3766d692943113555ae7648984f16fc6b5aa9057becd8d95d038ca95579fff5902497ccdf00064ad97fac6ebcab1bcd69139eec64dfcc9aa4d5b39e90abb2e3699105f60a3f8e13cfbdd583e37f3c63abbf009cfe5279fc84bb8db14ba61476ca929221a8d58c03e399471b660fd76c5decb3ebd456c54f3fcf4ec53125fb695f11bf93837016e231fdfd40ddcdb2d91cd679a4cca569bb5a6c8b72690a5cddb4267e22945b42532f1a2d2276e653d0798fc8d6c2b7a10e22b78859e66ac0e3da5b80a14613d3ce34b96a2671b20795bd68e390aab723ce14f615408543e1bdecc4c411aade12cf9ad034222afcdfc9a500a96472b248a313882bd80954f0a73ada59d7dabe73e8b30babde23b766a6c79482ec483070fc8f7b6f109362fc5e78cf51bbe391bcbf3b1bcf7762406dfdcfce1de3737a767f83dc7efbdc10e7db4c8703a121d93101154c2128911f2b043d3a80da912bccf6b85a19c81d80f07273083c4b788d10b71eb86b3436014e67a4ac2265fa8c2e99118464823215ee344c1d92d8242156bb5a1b482e8a1e496b8e4b5deac6d038266d9d2d1c8e4954628e88633563547e273aed514d485e20413222970db9ae370a6717460e8bf221fb1f8dccee968839abc6fb1d4445ae4862b8fbc55d2c72e9e1080fc0832cea81f3ffccbed51e9e6e3877f67f299df9ab82dbc81b5a2a3a3ff6da565e09543eccd6bb38da7141642951036b65dae90d2c14319a7d4f04b3e82067b61c2dc389271f93392f3ec8510b3762127e9753812a4a541d6928daa967a78767a3abaa07a02ba8c12b31eee065b2033a3d18865654eebebe1e9284511cc30e9bc9f2c330461d82912e5b46162e0eda0a2a97cf71d393d98f7b79da1a55dc879eb3cd40a5567536b216054327b81ea575be70c1528b8997822eb011372ee53dd68e6ab907314e556f63648b437a5ceba87ceac69e5c0ce0211b5688b696e504027f23b56e6c8e44717f26c1c5ee670a9d7b472b84165d7c3d1587c2f44ae173247facdf5347d1d223c8237a459c8d960da7d994e0764427c0f9fe9072f0070b80542aa4ed2de1c2937c5c2d1dbb13c3a7e7d5c1ee7af8e9f5e1c3fbf38beca8e17472396d668df362c3dc0d2d54fc0328e4b1ece1bad8f0f018cb610a3e0ef7635b990af9a568fe52060c33b0411ec05ef762bceef0f808a1039c911bb51db731210ea85c2019ceca93512d40030cd542d15cc3b39c863bd3dc6f1f04e57d65e4ff63c37daa605ce993d5264cda2985160a2d86af34e53d620817fe4fc13296b651ab7475f221593ad2955f56a6714032921b12d28339ebc53454b85116232f1980a2be70f8aff9f776a6257421851a779edee021597b87cf042886eb67b0c9f2ccbe41d21c46c19ff3de25a49f1401b928de5642207f9602ba01708c95dd0f715c887f7cfc781d7869c3e44d64a6c8d32e81c88ec71d3d86678f455758dd358b16617f2b83992c792f104a64e0d68cf8e19121a8e77b5ab4e4279ff7c17a44240f5541b428bd12e4146593b4265b9b729a9b62a8485a46d0fcf1630f9ef60dbb17c739651a9cdeea36920ba9896f7cf0db16f0f4cfc377074a9fcae5332ee2c35490fa31ea6c936d9d3f117e251fe8e4220dfd65658b2f28d45ffff8873b52abc6e2ad4f2771cae284e8d0a1d78578353418d1533cac2f4726dea314f1d53b6c2748547d05374f16aa9eab4166aaaa37986eb428935bfb64cc725a3f206f5238e62a14ed3b108b59b7671085542596052d0719c72a92d0d5e4b6709c430e9987a64b3ac2c771ec687bdd1b6f8b877c4945c4e9c555bce48634464980d1c95046e916a74adc97e4864ba19d378e8570a63869aaf1203d19359c9208c603771f067641d182eb5ebc9e73402c2a23557f2dcc0d498027802794cf28109389cee6053ea0a5d1491f1c4a7196a94c7218139954cccadb3daa6bd455bcd8913ade55d4d0d67973492e961ea2cc672a9fd144d229e6315aa487527bbe06bb23def0f4309dc6b8b025baf92fd0acd660127036890cf1ca4d1127eafc8921d2d9efb1b8687b80ffd4401931eda2178466267b72af4a7c13d34a224dba53884d50273c7c6cd636828fb2d5ffadc2122570eb7c4a1efdbc7874eafcafb5440cb6028a98a2f2dd645ec9c29cae3a01cbb34d4e882bcbc71e36efecaad7661b026d781af356e256f17415307e6a238839e85e3411fe265020d9714d07771a3da55aa84b11a45dd7e824061fdff37e73d501a32f919861ec3b5b3c220da87749e754f52e5396731c38520c80cbc69df467768c86d20e13d03e3cc841ac41303b69ae9b9a2ab876badebdbc6513e3bb1c784cbd9d28599217f91f8b572a153e0f8a4ca5dda86ce9eaaa28eb0c765db00922f9005d62b4ca17b372cd2b5351d38a8add71dbeae2e48a73dcf575b8fc66b271e89b96a3b06032c61a0496d0c1245b2d390ac00d1e99e81669ad7e1f645ce57740af98286340c9d088d72c032252c132a2169a62321a11ad0fedce5817e8221acf58b93dfe3df1e65aa1c77f7327794c637b381ab554973ec402f9783b7e34344a35b87e1ae3c266403c68566e0cd9e482152ab25c42b1e5ebbde6b1c26e2bd8e27d6bfd4ccfd0cb56e6ba166839bcdfbf79bc1e8a7f419897e1f32213e8acc49e2dd7a33064708de1842eca9d82584413e4ee694583876f742f2343bcf6e4854837e96b2c1c1f77b72a5d028385b62bb2a1d1c04778826b4c5cc971084c44a0179d705031d468e332e43d82093bd7b8aee82a223d7377561e606c78a15a062b65690b77386b3eda4d05506b606e38ecd782aa34842a16f75f245eb5f2c3ee7b2b93d4b57ded6cf525794edbe12fa2e1184d689eecb1aebeddc16d25654bed9da6a46f791bd8b14f1e41239a27705f4e412394b514258d325e9b2b16bce9b4561e79c86d27d17df08975a55aed7d5513310521a7e0bba157c72194c99a26339cf9096e9b619c6e02db6dc05ecdb355f99d8bbd011e22ff462822bb609eaefada61b214ee73054d0252465beb7228ec22c57dc6f516e2499bba076afab62a2dce26298bbb74ff1d8a98aaef27ae6e311b57fabcc0a203952a740beefdf849351d7e9266e119a3452012d4bcd85650f57a4df02732d3ab2642ff9a80a775aeb70afb74716d38749d3dec1349a891f5ccb8b3ffee2ac1b2b5326eb5d6c73d7e0a43c917c5548ff35f1f0e1ff00b5629026")); + assertThat(extended, notNullValue()); + assertThat(extended.getMessage().getSubject(), is("Extended encoding on Windows works - but how ??")); + assertThat(extended.getMessage().getBody(), notNullValue()); + assertThat(extended.getMessage().getBody().length(), is(6233)); + } + + + @Test + public void ensureSimpleMessageIsEncoded() { + ExtendedEncoding in = new ExtendedEncoding.Builder() + .message() + .subject("Test sübject") + .body("test bödy") + .build(); + + assertThat(in.zip(), notNullValue()); + ExtendedEncoding out = ExtendedEncoding.unzip(in.zip()); + + assertThat(out, is(in)); + } +} diff --git a/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java b/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java index 5405f23..899c60b 100644 --- a/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java @@ -17,6 +17,7 @@ package ch.dissem.bitmessage.entity; import ch.dissem.bitmessage.entity.payload.*; +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.factory.Factory; @@ -79,12 +80,37 @@ public class SerializationTest extends TestBase { @Test public void ensurePlaintextIsSerializedAndDeserializedCorrectly() throws Exception { Plaintext p1 = new Plaintext.Builder(MSG) - .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) - .to(TestUtils.loadContact()) - .message("Subject", "Message") - .ackData("ackMessage".getBytes()) - .signature(new byte[0]) - .build(); + .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) + .to(TestUtils.loadContact()) + .message("Subject", "Message") + .ackData("ackMessage".getBytes()) + .signature(new byte[0]) + .build(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + p1.write(out); + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + Plaintext p2 = Plaintext.read(MSG, in); + + // Received is automatically set on deserialization, so we'll need to set it to 0 + Field received = Plaintext.class.getDeclaredField("received"); + received.setAccessible(true); + received.set(p2, 0L); + + assertEquals(p1, p2); + } + + @Test + public void ensurePlaintextWithExtendedEncodingIsSerializedAndDeserializedCorrectly() throws Exception { + Plaintext p1 = new Plaintext.Builder(MSG) + .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) + .to(TestUtils.loadContact()) + .message(new ExtendedEncoding.Builder().message() + .subject("Subject") + .body("Message") + .build()) + .ackData("ackMessage".getBytes()) + .signature(new byte[0]) + .build(); ByteArrayOutputStream out = new ByteArrayOutputStream(); p1.write(out); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); @@ -101,12 +127,12 @@ public class SerializationTest extends TestBase { @Test public void ensurePlaintextWithAckMessageIsSerializedAndDeserializedCorrectly() throws Exception { Plaintext p1 = new Plaintext.Builder(MSG) - .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) - .to(TestUtils.loadContact()) - .message("Subject", "Message") - .ackData("ackMessage".getBytes()) - .signature(new byte[0]) - .build(); + .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) + .to(TestUtils.loadContact()) + .message("Subject", "Message") + .ackData("ackMessage".getBytes()) + .signature(new byte[0]) + .build(); ObjectMessage ackMessage1 = p1.getAckMessage(); assertNotNull(ackMessage1); @@ -156,11 +182,11 @@ public class SerializationTest extends TestBase { @Test public void ensureSystemSerializationWorks() throws Exception { Plaintext plaintext = new Plaintext.Builder(MSG) - .from(TestUtils.loadContact()) - .to(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) - .labels(Collections.singletonList(new Label("Test", Label.Type.INBOX, 0))) - .message("Test", "Test Test.\nTest") - .build(); + .from(TestUtils.loadContact()) + .to(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) + .labels(Collections.singletonList(new Label("Test", Label.Type.INBOX, 0))) + .message("Test", "Test Test.\nTest") + .build(); ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(out); oos.writeObject(plaintext); From 31eca20ccafe68833eebc721c4c3cc8685428033 Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Mon, 5 Dec 2016 07:41:55 +0100 Subject: [PATCH 02/22] Fixed NPE when 'from' doesn't have a public key. From will get lost in those cases unless it's saved separately. --- .../dissem/bitmessage/entity/Plaintext.java | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java index e08df3d..9837b23 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java @@ -178,12 +178,23 @@ public class Plaintext implements Streamable { public void write(OutputStream out, boolean includeSignature) throws IOException { Encode.varInt(from.getVersion(), out); Encode.varInt(from.getStream(), out); - Encode.int32(from.getPubkey().getBehaviorBitfield(), out); - out.write(from.getPubkey().getSigningKey(), 1, 64); - out.write(from.getPubkey().getEncryptionKey(), 1, 64); - if (from.getVersion() >= 3) { - Encode.varInt(from.getPubkey().getNonceTrialsPerByte(), out); - Encode.varInt(from.getPubkey().getExtraBytes(), out); + if (from.getPubkey() == null) { + Encode.int32(0, out); + byte[] empty = new byte[64]; + out.write(empty); + out.write(empty); + if (from.getVersion() >= 3) { + Encode.varInt(0, out); + Encode.varInt(0, out); + } + } else { + Encode.int32(from.getPubkey().getBehaviorBitfield(), out); + out.write(from.getPubkey().getSigningKey(), 1, 64); + out.write(from.getPubkey().getEncryptionKey(), 1, 64); + if (from.getVersion() >= 3) { + Encode.varInt(from.getPubkey().getNonceTrialsPerByte(), out); + Encode.varInt(from.getPubkey().getExtraBytes(), out); + } } if (type == Type.MSG) { out.write(to.getRipe()); @@ -213,12 +224,23 @@ public class Plaintext implements Streamable { public void write(ByteBuffer buffer, boolean includeSignature) { Encode.varInt(from.getVersion(), buffer); Encode.varInt(from.getStream(), buffer); - Encode.int32(from.getPubkey().getBehaviorBitfield(), buffer); - buffer.put(from.getPubkey().getSigningKey(), 1, 64); - buffer.put(from.getPubkey().getEncryptionKey(), 1, 64); - if (from.getVersion() >= 3) { - Encode.varInt(from.getPubkey().getNonceTrialsPerByte(), buffer); - Encode.varInt(from.getPubkey().getExtraBytes(), buffer); + if (from.getPubkey() == null) { + Encode.int32(0, buffer); + byte[] empty = new byte[64]; + buffer.put(empty); + buffer.put(empty); + if (from.getVersion() >= 3) { + Encode.varInt(0, buffer); + Encode.varInt(0, buffer); + } + } else { + Encode.int32(from.getPubkey().getBehaviorBitfield(), buffer); + buffer.put(from.getPubkey().getSigningKey(), 1, 64); + buffer.put(from.getPubkey().getEncryptionKey(), 1, 64); + if (from.getVersion() >= 3) { + Encode.varInt(from.getPubkey().getNonceTrialsPerByte(), buffer); + Encode.varInt(from.getPubkey().getExtraBytes(), buffer); + } } if (type == Type.MSG) { buffer.put(to.getRipe()); From e1dcbbf19c740c1bb26c3da2579f014225e8abdd Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Wed, 7 Dec 2016 20:01:19 +0100 Subject: [PATCH 03/22] Don't block when adding a new contact / searching for its public key --- .../ch/dissem/bitmessage/InternalContext.java | 55 +++++++++++-------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/ch/dissem/bitmessage/InternalContext.java b/core/src/main/java/ch/dissem/bitmessage/InternalContext.java index e01a90a..007e8f2 100644 --- a/core/src/main/java/ch/dissem/bitmessage/InternalContext.java +++ b/core/src/main/java/ch/dissem/bitmessage/InternalContext.java @@ -30,6 +30,8 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.TreeSet; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; /** * The internal context should normally only be used for port implementations. If you need it in your client @@ -45,6 +47,8 @@ public class InternalContext { public final static long NETWORK_NONCE_TRIALS_PER_BYTE = 1000; public final static long NETWORK_EXTRA_BYTES = 1000; + private final Executor threadPool = Executors.newCachedThreadPool(); + private final Cryptography cryptography; private final Inventory inventory; private final NodeRegistry nodeRegistry; @@ -226,31 +230,36 @@ public class InternalContext { * for freshly received pubkeys will not be called. Instead the pubkey is added to the contact and stored on DB. */ public void requestPubkey(final BitmessageAddress contact) { - BitmessageAddress stored = addressRepository.getAddress(contact.getAddress()); + threadPool.execute(new Runnable() { + @Override + public void run() { + BitmessageAddress stored = addressRepository.getAddress(contact.getAddress()); - tryToFindMatchingPubkey(contact); - if (contact.getPubkey() != null) { - if (stored != null) { - stored.setPubkey(contact.getPubkey()); - addressRepository.save(stored); - } else { - addressRepository.save(contact); + tryToFindMatchingPubkey(contact); + if (contact.getPubkey() != null) { + if (stored != null) { + stored.setPubkey(contact.getPubkey()); + addressRepository.save(stored); + } else { + addressRepository.save(contact); + } + return; + } + + if (stored == null) { + addressRepository.save(contact); + } + + long expires = UnixTime.now(TTL.getpubkey()); + LOG.info("Expires at " + expires); + final ObjectMessage request = new ObjectMessage.Builder() + .stream(contact.getStream()) + .expiresTime(expires) + .payload(new GetPubkey(contact)) + .build(); + proofOfWorkService.doProofOfWork(request); } - return; - } - - if (stored == null) { - addressRepository.save(contact); - } - - long expires = UnixTime.now(TTL.getpubkey()); - LOG.info("Expires at " + expires); - final ObjectMessage request = new ObjectMessage.Builder() - .stream(contact.getStream()) - .expiresTime(expires) - .payload(new GetPubkey(contact)) - .build(); - proofOfWorkService.doProofOfWork(request); + }); } private void tryToFindMatchingPubkey(BitmessageAddress address) { From 6d67598a40d56786d142c1711aee521d9b3479e0 Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Sun, 11 Dec 2016 14:02:32 +0100 Subject: [PATCH 04/22] First implementation of extended message encoding. Works as far as the PyBitmessage implementation, with some additional code. --- .../dissem/bitmessage/entity/Plaintext.java | 30 +- .../entity/valueobject/ExtendedEncoding.java | 170 ++---------- .../{ => extended}/Attachment.java | 9 +- .../entity/valueobject/extended/Message.java | 257 ++++++++++++++++++ .../entity/valueobject/extended/Vote.java | 131 +++++++++ .../factory/ExtendedEncodingFactory.java | 57 ++++ .../ch/dissem/bitmessage/utils/Bytes.java | 5 + .../entity/ExtendedEncodingTest.java | 63 ++++- .../bitmessage/entity/SerializationTest.java | 7 +- .../ch/dissem/bitmessage/utils/TestUtils.java | 10 + .../repository/JdbcMessageRepositoryTest.java | 9 +- 11 files changed, 567 insertions(+), 181 deletions(-) rename core/src/main/java/ch/dissem/bitmessage/entity/valueobject/{ => extended}/Attachment.java (91%) create mode 100644 core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java create mode 100644 core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Vote.java create mode 100644 core/src/main/java/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.java diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java index 9837b23..06c1584 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java @@ -18,11 +18,13 @@ package ch.dissem.bitmessage.entity; import ch.dissem.bitmessage.entity.payload.Msg; import ch.dissem.bitmessage.entity.payload.Pubkey.Feature; -import ch.dissem.bitmessage.entity.valueobject.Attachment; +import ch.dissem.bitmessage.entity.valueobject.extended.Attachment; import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; +import ch.dissem.bitmessage.entity.valueobject.extended.Message; import ch.dissem.bitmessage.exception.ApplicationException; +import ch.dissem.bitmessage.factory.ExtendedEncodingFactory; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.utils.Decode; import ch.dissem.bitmessage.utils.Encode; @@ -333,10 +335,10 @@ public class Plaintext implements Streamable { Scanner s = new Scanner(new ByteArrayInputStream(message), "UTF-8"); String firstLine = s.nextLine(); if (encoding == EXTENDED.code) { - if (getExtendedData().getMessage() == null) { - return null; + if (Message.TYPE.equals(getExtendedData().getType())) { + return ((Message) extendedData.getContent()).getSubject(); } else { - return extendedData.getMessage().getSubject(); + return null; } } else if (encoding == SIMPLE.code) { return firstLine.substring("Subject:".length()).trim(); @@ -349,10 +351,10 @@ public class Plaintext implements Streamable { public String getText() { if (encoding == EXTENDED.code) { - if (getExtendedData().getMessage() == null) { - return null; + if (Message.TYPE.equals(getExtendedData().getType())) { + return ((Message) extendedData.getContent()).getBody(); } else { - return extendedData.getMessage().getBody(); + return null; } } else { try { @@ -370,24 +372,24 @@ public class Plaintext implements Streamable { protected ExtendedEncoding getExtendedData() { if (extendedData == null && encoding == EXTENDED.code) { // TODO: make sure errors are properly handled - extendedData = ExtendedEncoding.unzip(message); + extendedData = ExtendedEncodingFactory.getInstance().unzip(message); } return extendedData; } public List getParents() { - if (getExtendedData() == null || extendedData.getMessage() == null) { - return Collections.emptyList(); + if (Message.TYPE.equals(getExtendedData().getType())) { + return ((Message) extendedData.getContent()).getParents(); } else { - return extendedData.getMessage().getParents(); + return Collections.emptyList(); } } public List getFiles() { - if (getExtendedData() == null || extendedData.getMessage() == null) { - return Collections.emptyList(); + if (Message.TYPE.equals(getExtendedData().getType())) { + return ((Message) extendedData.getContent()).getFiles(); } else { - return extendedData.getMessage().getFiles(); + return Collections.emptyList(); } } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java index 699cd22..f5a15c7 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java @@ -4,35 +4,38 @@ import ch.dissem.bitmessage.exception.ApplicationException; import org.msgpack.core.MessagePack; import org.msgpack.core.MessagePacker; import org.msgpack.core.MessageUnpacker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.Serializable; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; import java.util.Objects; import java.util.zip.DeflaterOutputStream; -import java.util.zip.InflaterInputStream; /** * Extended encoding message object. */ public class ExtendedEncoding implements Serializable { private static final long serialVersionUID = 3876871488247305200L; + private static final Logger LOG = LoggerFactory.getLogger(ExtendedEncoding.class); - private Message message; + private ExtendedType content; - public ExtendedEncoding(Message message) { - this.message = message; + public ExtendedEncoding(ExtendedType content) { + this.content = content; } - private ExtendedEncoding() { + public String getType() { + if (content == null) { + return null; + } else { + return content.getType(); + } } - public Message getMessage() { - return message; + public ExtendedType getContent() { + return content; } public byte[] zip() { @@ -40,10 +43,7 @@ public class ExtendedEncoding implements Serializable { DeflaterOutputStream zipper = new DeflaterOutputStream(out)) { MessagePacker packer = MessagePack.newDefaultPacker(zipper); - // FIXME: this should work for trivial cases - if (message != null) { - message.pack(packer); - } + content.pack(packer); packer.close(); zipper.close(); return out.toByteArray(); @@ -52,152 +52,28 @@ public class ExtendedEncoding implements Serializable { } } - public static ExtendedEncoding unzip(byte[] zippedData) { - ExtendedEncoding result = new ExtendedEncoding(); - try (InflaterInputStream unzipper = new InflaterInputStream(new ByteArrayInputStream(zippedData))) { - MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(unzipper); - int mapSize = unpacker.unpackMapHeader(); - for (int i = 0; i < mapSize; i++) { - String key = unpacker.unpackString(); - switch (key) { - case "": - switch (unpacker.unpackString()) { - case "message": - result.message = new Message(); - break; - } - break; - case "subject": - result.message.subject = unpacker.unpackString(); - break; - case "body": - result.message.body = unpacker.unpackString(); - break; - default: - break; - } - } - } catch (IOException e) { - throw new ApplicationException(e); - } - return result; - } - @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ExtendedEncoding that = (ExtendedEncoding) o; - return Objects.equals(message, that.message); + return Objects.equals(content, that.content); } @Override public int hashCode() { - return Objects.hash(message); + return Objects.hash(content); } - public static class Message implements Serializable { - private static final long serialVersionUID = -2724977231484285467L; + public interface Unpacker { + String getType(); - private String subject; - private String body; - private List parents; - private List files; - - private Message() { - parents = Collections.emptyList(); - files = Collections.emptyList(); - } - - private Message(Builder builder) { - subject = builder.subject; - body = builder.body; - parents = Collections.unmodifiableList(builder.parents); - files = Collections.unmodifiableList(builder.files); - } - - public String getSubject() { - return subject; - } - - public String getBody() { - return body; - } - - public List getParents() { - return parents; - } - - public List getFiles() { - return files; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Message message = (Message) o; - return Objects.equals(subject, message.subject) && - Objects.equals(body, message.body) && - Objects.equals(parents, message.parents) && - Objects.equals(files, message.files); - } - - @Override - public int hashCode() { - return Objects.hash(subject, body, parents, files); - } - - public void pack(MessagePacker packer) throws IOException { - packer.packMapHeader(3); - packer.packString(""); - packer.packString("message"); - packer.packString("subject"); - packer.packString(subject); - packer.packString("body"); - packer.packString(body); - } - - public static class Builder { - private String subject; - private String body; - private List parents = new LinkedList<>(); - private List files = new LinkedList<>(); - - private Builder() { - } - - public Builder subject(String subject) { - this.subject = subject; - return this; - } - - public Builder body(String body) { - this.body = body; - return this; - } - - public Builder addParent(InventoryVector iv) { - parents.add(iv); - return this; - } - - public Builder addFile(Attachment file) { - files.add(file); - return this; - } - - public ExtendedEncoding build() { - return new ExtendedEncoding(new Message(this)); - } - } + T unpack(MessageUnpacker unpacker, int size); } - public static class Builder { - public Message.Builder message() { - return new Message.Builder(); - } + public interface ExtendedType extends Serializable { + String getType(); - // TODO: vote (etc.?) + void pack(MessagePacker packer) throws IOException; } } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Attachment.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Attachment.java similarity index 91% rename from core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Attachment.java rename to core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Attachment.java index 8ea64de..42afc4f 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Attachment.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Attachment.java @@ -1,4 +1,4 @@ -package ch.dissem.bitmessage.entity.valueobject; +package ch.dissem.bitmessage.entity.valueobject.extended; import java.io.Serializable; import java.util.Arrays; @@ -48,7 +48,7 @@ public class Attachment implements Serializable { return Objects.hash(name, data, type, disposition); } - private enum Disposition { + public enum Disposition { inline, attachment } @@ -83,6 +83,11 @@ public class Attachment implements Serializable { return this; } + public Builder disposition(Disposition disposition) { + this.disposition = disposition; + return this; + } + public Attachment build() { Attachment attachment = new Attachment(); attachment.type = this.type; diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java new file mode 100644 index 0000000..33e7b81 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java @@ -0,0 +1,257 @@ +package ch.dissem.bitmessage.entity.valueobject.extended; + +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import org.msgpack.core.MessagePacker; +import org.msgpack.core.MessageUnpacker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.URLConnection; +import java.nio.file.Files; +import java.util.*; + +/** + * Extended encoding type 'message'. Properties 'parents' and 'files' not yet supported by PyBitmessage, so they might not work + * properly with future PyBitmessage implementations. + */ +public class Message implements ExtendedEncoding.ExtendedType { + private static final long serialVersionUID = -2724977231484285467L; + private static final Logger LOG = LoggerFactory.getLogger(Message.class); + + public static final String TYPE = "message"; + + private String subject; + private String body; + private List parents; + private List files; + + private Message(Builder builder) { + subject = builder.subject; + body = builder.body; + parents = Collections.unmodifiableList(builder.parents); + files = Collections.unmodifiableList(builder.files); + } + + @Override + public String getType() { + return TYPE; + } + + public String getSubject() { + return subject; + } + + public String getBody() { + return body; + } + + public List getParents() { + return parents; + } + + public List getFiles() { + return files; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Message message = (Message) o; + return Objects.equals(subject, message.subject) && + Objects.equals(body, message.body) && + Objects.equals(parents, message.parents) && + Objects.equals(files, message.files); + } + + @Override + public int hashCode() { + return Objects.hash(subject, body, parents, files); + } + + public void pack(MessagePacker packer) throws IOException { + int size = 3; + if (!files.isEmpty()) { + size++; + } + if (!parents.isEmpty()) { + size++; + } + packer.packMapHeader(size); + packer.packString(""); + packer.packString("message"); + packer.packString("subject"); + packer.packString(subject); + packer.packString("body"); + packer.packString(body); + if (!files.isEmpty()) { + packer.packString("files"); + packer.packArrayHeader(files.size()); + for (Attachment file : files) { + packer.packMapHeader(4); + packer.packString("name"); + packer.packString(file.getName()); + packer.packString("data"); + packer.packBinaryHeader(file.getData().length); + packer.writePayload(file.getData()); + packer.packString("type"); + packer.packString(file.getType()); + packer.packString("disposition"); + packer.packString(file.getDisposition().name()); + } + } + if (!parents.isEmpty()) { + packer.packString("parents"); + packer.packArrayHeader(parents.size()); + for (InventoryVector parent : parents) { + packer.packBinaryHeader(parent.getHash().length); + packer.writePayload(parent.getHash()); + } + } + } + + public static class Builder { + private String subject; + private String body; + private List parents = new LinkedList<>(); + private List files = new LinkedList<>(); + + public Builder subject(String subject) { + this.subject = subject; + return this; + } + + public Builder body(String body) { + this.body = body; + return this; + } + + public Builder addParent(Plaintext parent) { + parents.add(parent.getInventoryVector()); + return this; + } + + public Builder addParent(InventoryVector iv) { + parents.add(iv); + return this; + } + + public Builder addFile(File file, Attachment.Disposition disposition) { + try { + files.add(new Attachment.Builder() + .name(file.getName()) + .disposition(disposition) + .type(URLConnection.guessContentTypeFromStream(new FileInputStream(file))) + .data(Files.readAllBytes(file.toPath())) + .build()); + } catch (IOException e) { + LOG.error(e.getMessage(), e); + } + return this; + } + + public Builder addFile(Attachment file) { + files.add(file); + return this; + } + + public ExtendedEncoding build() { + return new ExtendedEncoding(new Message(this)); + } + } + + public static class Unpacker implements ExtendedEncoding.Unpacker { + @Override + public String getType() { + return TYPE; + } + + @Override + public Message unpack(MessageUnpacker unpacker, int size) { + Message.Builder builder = new Message.Builder(); + try { + for (int i = 0; i < size; i++) { + String key = unpacker.unpackString(); + switch (key) { + case "subject": + builder.subject(unpacker.unpackString()); + break; + case "body": + builder.body(unpacker.unpackString()); + break; + case "parents": + builder.parents = unpackParents(unpacker); + break; + case "files": + builder.files = unpackFiles(unpacker); + break; + default: + LOG.error("Unexpected data with key: " + key); + break; + } + } + } catch (IOException e) { + LOG.error(e.getMessage(), e); + } + return new Message(builder); + } + + private static List unpackParents(MessageUnpacker unpacker) throws IOException { + int size = unpacker.unpackArrayHeader(); + List parents = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + int binarySize = unpacker.unpackBinaryHeader(); + parents.add(new InventoryVector(unpacker.readPayload(binarySize))); + } + return parents; + } + + private static List unpackFiles(MessageUnpacker unpacker) throws IOException { + int size = unpacker.unpackArrayHeader(); + List files = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + Attachment.Builder attachment = new Attachment.Builder(); + int mapSize = unpacker.unpackMapHeader(); + for (int j = 0; j < mapSize; j++) { + String key = unpacker.unpackString(); + switch (key) { + case "name": + attachment.name(unpacker.unpackString()); + break; + case "data": + int binarySize = unpacker.unpackBinaryHeader(); + attachment.data(unpacker.readPayload(binarySize)); + break; + case "type": + attachment.type(unpacker.unpackString()); + break; + case "disposition": + String disposition = unpacker.unpackString(); + switch (disposition) { + case "inline": + attachment.inline(); + break; + case "attachment": + attachment.attachment(); + break; + default: + LOG.debug("Unknown disposition: " + disposition); + break; + } + break; + default: + LOG.debug("Unknown file info '" + key + "' with data: " + unpacker.unpackValue()); + break; + } + } + files.add(attachment.build()); + } + return files; + } + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Vote.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Vote.java new file mode 100644 index 0000000..75cea67 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Vote.java @@ -0,0 +1,131 @@ +package ch.dissem.bitmessage.entity.valueobject.extended; + +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import org.msgpack.core.MessagePacker; +import org.msgpack.core.MessageUnpacker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Objects; + +/** + * Extended encoding type 'vote'. Specification still outstanding, so this will need some work. + */ +public class Vote implements ExtendedEncoding.ExtendedType { + private static final long serialVersionUID = -8427038604209964837L; + private static final Logger LOG = LoggerFactory.getLogger(Vote.class); + + public static final String TYPE = "vote"; + + private InventoryVector msgId; + private String vote; + + private Vote(Builder builder) { + msgId = builder.msgId; + vote = builder.vote; + } + + @Override + public String getType() { + return TYPE; + } + + public InventoryVector getMsgId() { + return msgId; + } + + public String getVote() { + return vote; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Vote vote1 = (Vote) o; + return Objects.equals(msgId, vote1.msgId) && + Objects.equals(vote, vote1.vote); + } + + @Override + public int hashCode() { + return Objects.hash(msgId, vote); + } + + public void pack(MessagePacker packer) throws IOException { + packer.packMapHeader(3); + packer.packString(""); + packer.packString("vote"); + packer.packString("msgId"); + packer.packBinaryHeader(msgId.getHash().length); + packer.writePayload(msgId.getHash()); + packer.packString("vote"); + packer.packString(vote); + } + + public static class Builder { + private InventoryVector msgId; + private String vote; + + public ExtendedEncoding up(Plaintext message) { + msgId = message.getInventoryVector(); + vote = "1"; + return new ExtendedEncoding(new Vote(this)); + } + + public ExtendedEncoding down(Plaintext message) { + msgId = message.getInventoryVector(); + vote = "1"; + return new ExtendedEncoding(new Vote(this)); + } + + public Builder msgId(InventoryVector iv) { + this.msgId = iv; + return this; + } + + public Builder vote(String vote) { + this.vote = vote; + return this; + } + + public ExtendedEncoding build() { + return new ExtendedEncoding(new Vote(this)); + } + } + + public static class Unpacker implements ExtendedEncoding.Unpacker { + @Override + public String getType() { + return TYPE; + } + + @Override + public Vote unpack(MessageUnpacker unpacker, int size) { + Vote.Builder builder = new Vote.Builder(); + try { + for (int i = 0; i < size; i++) { + String key = unpacker.unpackString(); + switch (key) { + case "msgId": + int binarySize = unpacker.unpackBinaryHeader(); + builder.msgId(new InventoryVector(unpacker.readPayload(binarySize))); + break; + case "vote": + builder.vote(unpacker.unpackString()); + break; + default: + LOG.error("Unexpected data with key: " + key); + break; + } + } + } catch (IOException e) { + LOG.error(e.getMessage(), e); + } + return new Vote(builder); + } + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.java b/core/src/main/java/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.java new file mode 100644 index 0000000..0dc419c --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.java @@ -0,0 +1,57 @@ +package ch.dissem.bitmessage.factory; + +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; +import ch.dissem.bitmessage.entity.valueobject.extended.Message; +import ch.dissem.bitmessage.entity.valueobject.extended.Vote; +import ch.dissem.bitmessage.exception.ApplicationException; +import org.msgpack.core.MessagePack; +import org.msgpack.core.MessageUnpacker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.InflaterInputStream; + +/** + * Factory that creates {@link ExtendedEncoding} objects from byte arrays. You can register your own types by adding a + * {@link ExtendedEncoding.Unpacker} using {@link #registerFactory(ExtendedEncoding.Unpacker)}. + */ +public class ExtendedEncodingFactory { + private static final Logger LOG = LoggerFactory.getLogger(ExtendedEncodingFactory.class); + private static final ExtendedEncodingFactory INSTANCE = new ExtendedEncodingFactory(); + private Map> factories = new HashMap<>(); + + private ExtendedEncodingFactory() { + registerFactory(new Message.Unpacker()); + registerFactory(new Vote.Unpacker()); + } + + public void registerFactory(ExtendedEncoding.Unpacker factory) { + factories.put(factory.getType(), factory); + } + + + public ExtendedEncoding unzip(byte[] zippedData) { + try (InflaterInputStream unzipper = new InflaterInputStream(new ByteArrayInputStream(zippedData))) { + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(unzipper); + int mapSize = unpacker.unpackMapHeader(); + String key = unpacker.unpackString(); + if (!"".equals(key)) { + LOG.error("Unexpected content: " + key); + return null; + } + String type = unpacker.unpackString(); + ExtendedEncoding.Unpacker factory = factories.get(type); + return new ExtendedEncoding(factory.unpack(unpacker, mapSize - 1)); + } catch (IOException e) { + throw new ApplicationException(e); + } + } + + public static ExtendedEncodingFactory getInstance() { + return INSTANCE; + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java b/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java index 986c288..c0bf554 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java @@ -16,6 +16,11 @@ package ch.dissem.bitmessage.utils; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; + /** * A helper class for working with byte arrays interpreted as unsigned big endian integers. * This is one part due to the fact that Java doesn't support unsigned numbers, and another diff --git a/core/src/test/java/ch/dissem/bitmessage/entity/ExtendedEncodingTest.java b/core/src/test/java/ch/dissem/bitmessage/entity/ExtendedEncodingTest.java index b4a1017..c417df1 100644 --- a/core/src/test/java/ch/dissem/bitmessage/entity/ExtendedEncodingTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/entity/ExtendedEncodingTest.java @@ -1,39 +1,82 @@ package ch.dissem.bitmessage.entity; import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; +import ch.dissem.bitmessage.entity.valueobject.extended.Attachment; +import ch.dissem.bitmessage.entity.valueobject.extended.Message; +import ch.dissem.bitmessage.entity.valueobject.extended.Vote; +import ch.dissem.bitmessage.factory.ExtendedEncodingFactory; import ch.dissem.bitmessage.utils.Bytes; +import ch.dissem.bitmessage.utils.TestUtils; import org.junit.Test; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; +import java.io.UnsupportedEncodingException; + +import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; /** * @author Christian Basler */ public class ExtendedEncodingTest { + private ExtendedEncodingFactory extendedEncodingFactory = ExtendedEncodingFactory.getInstance(); @Test public void ensureSimpleMessageIsDecoded() { - ExtendedEncoding extended = ExtendedEncoding.unzip(Bytes.fromHex("78da9d59dd8e1bb715ee359fa097c4b65b498876babb76d0760bd970368eedb68e8daed3c0700c81d25012b333c3e99063ad1c0430d0a728d002bd289077caa59fa4df39244723693705b2f0da339c730ebff3c3f343ffe33f3f94da39b5d4ff9dd97cf3e32f9d10524afe4b3a2b57762dfd4acb453bbf967355c967b2755a1a2f1f0afe791eb85f2a7c374e2ab950ce8fe5dc96b59a7b3933956a36d2e9c6a8c2bc57ded84a2e6c532a10b9d678352b342d48674a53a846e6ca2be9adfcd3d58b2f33f96a05a190748d3d64ddd87726d74e5ebedcf81504417a6eaaa563018d56f4225595cb75633c3df7d191e04c3cab9c574521c4af656d6a69c2ab2cdd927639a959b0102f372f3742ecae6ef7afdb0668c2a229eb4297baf2ac5b2689932d4576f2809f89af01d3ae61d9af57ba921bdbd2f78f1ffee999269a2837ce3766d692943113555ae7648984f16fc6b5aa9057becd8d95d038ca95579fff5902497ccdf00064ad97fac6ebcab1bcd69139eec64dfcc9aa4d5b39e90abb2e3699105f60a3f8e13cfbdd583e37f3c63abbf009cfe5279fc84bb8db14ba61476ca929221a8d58c03e399471b660fd76c5decb3ebd456c54f3fcf4ec53125fb695f11bf93837016e231fdfd40ddcdb2d91cd679a4cca569bb5a6c8b72690a5cddb4267e22945b42532f1a2d2276e653d0798fc8d6c2b7a10e22b78859e66ac0e3da5b80a14613d3ce34b96a2671b20795bd68e390aab723ce14f615408543e1bdecc4c411aade12cf9ad034222afcdfc9a500a96472b248a313882bd80954f0a73ada59d7dabe73e8b30babde23b766a6c79482ec483070fc8f7b6f109362fc5e78cf51bbe391bcbf3b1bcf7762406dfdcfce1de3737a767f83dc7efbdc10e7db4c8703a121d93101154c2128911f2b043d3a80da912bccf6b85a19c81d80f07273083c4b788d10b71eb86b3436014e67a4ac2265fa8c2e99118464823215ee344c1d92d8242156bb5a1b482e8a1e496b8e4b5deac6d038266d9d2d1c8e4954628e88633563547e273aed514d485e20413222970db9ae370a6717460e8bf221fb1f8dccee968839abc6fb1d4445ae4862b8fbc55d2c72e9e1080fc0832cea81f3ffccbed51e9e6e3877f67f299df9ab82dbc81b5a2a3a3ff6da565e09543eccd6bb38da7141642951036b65dae90d2c14319a7d4f04b3e82067b61c2dc389271f93392f3ec8510b3762127e9753812a4a541d6928daa967a78767a3abaa07a02ba8c12b31eee065b2033a3d18865654eebebe1e9284511cc30e9bc9f2c330461d82912e5b46162e0eda0a2a97cf71d393d98f7b79da1a55dc879eb3cd40a5567536b216054327b81ea575be70c1528b8997822eb011372ee53dd68e6ab907314e556f63648b437a5ceba87ceac69e5c0ce0211b5688b696e504027f23b56e6c8e44717f26c1c5ee670a9d7b472b84165d7c3d1587c2f44ae173247facdf5347d1d223c8237a459c8d960da7d994e0764427c0f9fe9072f0070b80542aa4ed2de1c2937c5c2d1dbb13c3a7e7d5c1ee7af8e9f5e1c3fbf38beca8e17472396d668df362c3dc0d2d54fc0328e4b1ece1bad8f0f018cb610a3e0ef7635b990af9a568fe52060c33b0411ec05ef762bceef0f808a1039c911bb51db731210ea85c2019ceca93512d40030cd542d15cc3b39c863bd3dc6f1f04e57d65e4ff63c37daa605ce993d5264cda2985160a2d86af34e53d620817fe4fc13296b651ab7475f221593ad2955f56a6714032921b12d28339ebc53454b85116232f1980a2be70f8aff9f776a6257421851a779edee021597b87cf042886eb67b0c9f2ccbe41d21c46c19ff3de25a49f1401b928de5642207f9602ba01708c95dd0f715c887f7cfc781d7869c3e44d64a6c8d32e81c88ec71d3d86678f455758dd358b16617f2b83992c792f104a64e0d68cf8e19121a8e77b5ab4e4279ff7c17a44240f5541b428bd12e4146593b4265b9b729a9b62a8485a46d0fcf1630f9ef60dbb17c739651a9cdeea36920ba9896f7cf0db16f0f4cfc377074a9fcae5332ee2c35490fa31ea6c936d9d3f117e251fe8e4220dfd65658b2f28d45ffff8873b52abc6e2ad4f2771cae284e8d0a1d78578353418d1533cac2f4726dea314f1d53b6c2748547d05374f16aa9eab4166aaaa37986eb428935bfb64cc725a3f206f5238e62a14ed3b108b59b7671085542596052d0719c72a92d0d5e4b6709c430e9987a64b3ac2c771ec687bdd1b6f8b877c4945c4e9c555bce48634464980d1c95046e916a74adc97e4864ba19d378e8570a63869aaf1203d19359c9208c603771f067641d182eb5ebc9e73402c2a23557f2dcc0d498027802794cf28109389cee6053ea0a5d1491f1c4a7196a94c7218139954cccadb3daa6bd455bcd8913ade55d4d0d67973492e961ea2cc672a9fd144d229e6315aa487527bbe06bb23def0f4309dc6b8b025baf92fd0acd660127036890cf1ca4d1127eafc8921d2d9efb1b8687b80ffd4401931eda2178466267b72af4a7c13d34a224dba53884d50273c7c6cd636828fb2d5ffadc2122570eb7c4a1efdbc7874eafcafb5440cb6028a98a2f2dd645ec9c29cae3a01cbb34d4e882bcbc71e36efecaad7661b026d781af356e256f17415307e6a238839e85e3411fe265020d9714d07771a3da55aa84b11a45dd7e824061fdff37e73d501a32f919861ec3b5b3c220da87749e754f52e5396731c38520c80cbc69df467768c86d20e13d03e3cc841ac41303b69ae9b9a2ab876badebdbc6513e3bb1c784cbd9d28599217f91f8b572a153e0f8a4ca5dda86ce9eaaa28eb0c765db00922f9005d62b4ca17b372cd2b5351d38a8add71dbeae2e48a73dcf575b8fc66b271e89b96a3b06032c61a0496d0c1245b2d390ac00d1e99e81669ad7e1f645ce57740af98286340c9d088d72c032252c132a2169a62321a11ad0fedce5817e8221acf58b93dfe3df1e65aa1c77f7327794c637b381ab554973ec402f9783b7e34344a35b87e1ae3c266403c68566e0cd9e482152ab25c42b1e5ebbde6b1c26e2bd8e27d6bfd4ccfd0cb56e6ba166839bcdfbf79bc1e8a7f419897e1f32213e8acc49e2dd7a33064708de1842eca9d82584413e4ee694583876f742f2343bcf6e4854837e96b2c1c1f77b72a5d028385b62bb2a1d1c04778826b4c5cc971084c44a0179d705031d468e332e43d82093bd7b8aee82a223d7377561e606c78a15a062b65690b77386b3eda4d05506b606e38ecd782aa34842a16f75f245eb5f2c3ee7b2b93d4b57ded6cf525794edbe12fa2e1184d689eecb1aebeddc16d25654bed9da6a46f791bd8b14f1e41239a27705f4e412394b514258d325e9b2b16bce9b4561e79c86d27d17df08975a55aed7d5513310521a7e0bba157c72194c99a26339cf9096e9b619c6e02db6dc05ecdb355f99d8bbd011e22ff462822bb609eaefada61b214ee73054d0252465beb7228ec22c57dc6f516e2499bba076afab62a2dce26298bbb74ff1d8a98aaef27ae6e311b57fabcc0a203952a740beefdf849351d7e9266e119a3452012d4bcd85650f57a4df02732d3ab2642ff9a80a775aeb70afb74716d38749d3dec1349a891f5ccb8b3ffee2ac1b2b5326eb5d6c73d7e0a43c917c5548ff35f1f0e1ff00b5629026")); - assertThat(extended, notNullValue()); - assertThat(extended.getMessage().getSubject(), is("Extended encoding on Windows works - but how ??")); - assertThat(extended.getMessage().getBody(), notNullValue()); - assertThat(extended.getMessage().getBody().length(), is(6233)); + ExtendedEncoding extended = extendedEncodingFactory.unzip(Bytes.fromHex("78da9d59dd8e1bb715ee359fa097c4b65b498876babb76d0760bd970368eedb68e8daed3c0700c81d25012b333c3e99063ad1c0430d0a728d002bd289077caa59fa4df39244723693705b2f0da339c730ebff3c3f343ffe33f3f94da39b5d4ff9dd97cf3e32f9d10524afe4b3a2b57762dfd4acb453bbf967355c967b2755a1a2f1f0afe791eb85f2a7c374e2ab950ce8fe5dc96b59a7b3933956a36d2e9c6a8c2bc57ded84a2e6c532a10b9d678352b342d48674a53a846e6ca2be9adfcd3d58b2f33f96a05a190748d3d64ddd87726d74e5ebedcf81504417a6eaaa563018d56f4225595cb75633c3df7d191e04c3cab9c574521c4af656d6a69c2ab2cdd927639a959b0102f372f3742ecae6ef7afdb0668c2a229eb4297baf2ac5b2689932d4576f2809f89af01d3ae61d9af57ba921bdbd2f78f1ffee999269a2837ce3766d692943113555ae7648984f16fc6b5aa9057becd8d95d038ca95579fff5902497ccdf00064ad97fac6ebcab1bcd69139eec64dfcc9aa4d5b39e90abb2e3699105f60a3f8e13cfbdd583e37f3c63abbf009cfe5279fc84bb8db14ba61476ca929221a8d58c03e399471b660fd76c5decb3ebd456c54f3fcf4ec53125fb695f11bf93837016e231fdfd40ddcdb2d91cd679a4cca569bb5a6c8b72690a5cddb4267e22945b42532f1a2d2276e653d0798fc8d6c2b7a10e22b78859e66ac0e3da5b80a14613d3ce34b96a2671b20795bd68e390aab723ce14f615408543e1bdecc4c411aade12cf9ad034222afcdfc9a500a96472b248a313882bd80954f0a73ada59d7dabe73e8b30babde23b766a6c79482ec483070fc8f7b6f109362fc5e78cf51bbe391bcbf3b1bcf7762406dfdcfce1de3737a767f83dc7efbdc10e7db4c8703a121d93101154c2128911f2b043d3a80da912bccf6b85a19c81d80f07273083c4b788d10b71eb86b3436014e67a4ac2265fa8c2e99118464823215ee344c1d92d8242156bb5a1b482e8a1e496b8e4b5deac6d038266d9d2d1c8e4954628e88633563547e273aed514d485e20413222970db9ae370a6717460e8bf221fb1f8dccee968839abc6fb1d4445ae4862b8fbc55d2c72e9e1080fc0832cea81f3ffccbed51e9e6e3877f67f299df9ab82dbc81b5a2a3a3ff6da565e09543eccd6bb38da7141642951036b65dae90d2c14319a7d4f04b3e82067b61c2dc389271f93392f3ec8510b3762127e9753812a4a541d6928daa967a78767a3abaa07a02ba8c12b31eee065b2033a3d18865654eebebe1e9284511cc30e9bc9f2c330461d82912e5b46162e0eda0a2a97cf71d393d98f7b79da1a55dc879eb3cd40a5567536b216054327b81ea575be70c1528b8997822eb011372ee53dd68e6ab907314e556f63648b437a5ceba87ceac69e5c0ce0211b5688b696e504027f23b56e6c8e44717f26c1c5ee670a9d7b472b84165d7c3d1587c2f44ae173247facdf5347d1d223c8237a459c8d960da7d994e0764427c0f9fe9072f0070b80542aa4ed2de1c2937c5c2d1dbb13c3a7e7d5c1ee7af8e9f5e1c3fbf38beca8e17472396d668df362c3dc0d2d54fc0328e4b1ece1bad8f0f018cb610a3e0ef7635b990af9a568fe52060c33b0411ec05ef762bceef0f808a1039c911bb51db731210ea85c2019ceca93512d40030cd542d15cc3b39c863bd3dc6f1f04e57d65e4ff63c37daa605ce993d5264cda2985160a2d86af34e53d620817fe4fc13296b651ab7475f221593ad2955f56a6714032921b12d28339ebc53454b85116232f1980a2be70f8aff9f776a6257421851a779edee021597b87cf042886eb67b0c9f2ccbe41d21c46c19ff3de25a49f1401b928de5642207f9602ba01708c95dd0f715c887f7cfc781d7869c3e44d64a6c8d32e81c88ec71d3d86678f455758dd358b16617f2b83992c792f104a64e0d68cf8e19121a8e77b5ab4e4279ff7c17a44240f5541b428bd12e4146593b4265b9b729a9b62a8485a46d0fcf1630f9ef60dbb17c739651a9cdeea36920ba9896f7cf0db16f0f4cfc377074a9fcae5332ee2c35490fa31ea6c936d9d3f117e251fe8e4220dfd65658b2f28d45ffff8873b52abc6e2ad4f2771cae284e8d0a1d78578353418d1533cac2f4726dea314f1d53b6c2748547d05374f16aa9eab4166aaaa37986eb428935bfb64cc725a3f206f5238e62a14ed3b108b59b7671085542596052d0719c72a92d0d5e4b6709c430e9987a64b3ac2c771ec687bdd1b6f8b877c4945c4e9c555bce48634464980d1c95046e916a74adc97e4864ba19d378e8570a63869aaf1203d19359c9208c603771f067641d182eb5ebc9e73402c2a23557f2dcc0d498027802794cf28109389cee6053ea0a5d1491f1c4a7196a94c7218139954cccadb3daa6bd455bcd8913ade55d4d0d67973492e961ea2cc672a9fd144d229e6315aa487527bbe06bb23def0f4309dc6b8b025baf92fd0acd660127036890cf1ca4d1127eafc8921d2d9efb1b8687b80ffd4401931eda2178466267b72af4a7c13d34a224dba53884d50273c7c6cd636828fb2d5ffadc2122570eb7c4a1efdbc7874eafcafb5440cb6028a98a2f2dd645ec9c29cae3a01cbb34d4e882bcbc71e36efecaad7661b026d781af356e256f17415307e6a238839e85e3411fe265020d9714d07771a3da55aa84b11a45dd7e824061fdff37e73d501a32f919861ec3b5b3c220da87749e754f52e5396731c38520c80cbc69df467768c86d20e13d03e3cc841ac41303b69ae9b9a2ab876badebdbc6513e3bb1c784cbd9d28599217f91f8b572a153e0f8a4ca5dda86ce9eaaa28eb0c765db00922f9005d62b4ca17b372cd2b5351d38a8add71dbeae2e48a73dcf575b8fc66b271e89b96a3b06032c61a0496d0c1245b2d390ac00d1e99e81669ad7e1f645ce57740af98286340c9d088d72c032252c132a2169a62321a11ad0fedce5817e8221acf58b93dfe3df1e65aa1c77f7327794c637b381ab554973ec402f9783b7e34344a35b87e1ae3c266403c68566e0cd9e482152ab25c42b1e5ebbde6b1c26e2bd8e27d6bfd4ccfd0cb56e6ba166839bcdfbf79bc1e8a7f419897e1f32213e8acc49e2dd7a33064708de1842eca9d82584413e4ee694583876f742f2343bcf6e4854837e96b2c1c1f77b72a5d028385b62bb2a1d1c04778826b4c5cc971084c44a0179d705031d468e332e43d82093bd7b8aee82a223d7377561e606c78a15a062b65690b77386b3eda4d05506b606e38ecd782aa34842a16f75f245eb5f2c3ee7b2b93d4b57ded6cf525794edbe12fa2e1184d689eecb1aebeddc16d25654bed9da6a46f791bd8b14f1e41239a27705f4e412394b514258d325e9b2b16bce9b4561e79c86d27d17df08975a55aed7d5513310521a7e0bba157c72194c99a26339cf9096e9b619c6e02db6dc05ecdb355f99d8bbd011e22ff462822bb609eaefada61b214ee73054d0252465beb7228ec22c57dc6f516e2499bba076afab62a2dce26298bbb74ff1d8a98aaef27ae6e311b57fabcc0a203952a740beefdf849351d7e9266e119a3452012d4bcd85650f57a4df02732d3ab2642ff9a80a775aeb70afb74716d38749d3dec1349a891f5ccb8b3ffee2ac1b2b5326eb5d6c73d7e0a43c917c5548ff35f1f0e1ff00b5629026")); + assertThat(extended, instanceOf(ExtendedEncoding.class)); + assertThat(extended.getContent(), instanceOf(Message.class)); + assertThat(((Message) extended.getContent()).getSubject(), is("Extended encoding on Windows works - but how ??")); + assertThat(((Message) extended.getContent()).getBody(), notNullValue()); + assertThat(((Message) extended.getContent()).getBody().length(), is(6233)); } @Test public void ensureSimpleMessageIsEncoded() { - ExtendedEncoding in = new ExtendedEncoding.Builder() - .message() + ExtendedEncoding in = new Message.Builder() .subject("Test sübject") .body("test bödy") .build(); assertThat(in.zip(), notNullValue()); - ExtendedEncoding out = ExtendedEncoding.unzip(in.zip()); + ExtendedEncoding out = extendedEncodingFactory.unzip(in.zip()); + assertThat(out, is(in)); + } + + @Test + public void ensureCompleteMessageIsEncodedAndDecoded() throws UnsupportedEncodingException { + ExtendedEncoding in = new Message.Builder() + .addParent(TestUtils.randomInventoryVector()) + .addParent(TestUtils.randomInventoryVector()) + .subject("Test sübject") + .body("test bödy") + .addFile( + new Attachment.Builder() + .name("test.txt") + .type("text/plain") + .data("test".getBytes("UTF-8")) + .attachment() + .build() + ) + .build(); + + assertThat(in.zip(), notNullValue()); + + ExtendedEncoding out = extendedEncodingFactory.unzip(in.zip()); + assertThat(out, is(in)); + } + + @Test + public void ensureVoteIsEncodedAndDecoded() { + ExtendedEncoding in = new Vote.Builder() + .msgId(TestUtils.randomInventoryVector()) + .vote("+1") + .build(); + + assertThat(in.zip(), notNullValue()); + + ExtendedEncoding out = extendedEncodingFactory.unzip(in.zip()); assertThat(out, is(in)); } } diff --git a/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java b/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java index 899c60b..aacc00f 100644 --- a/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java @@ -17,9 +17,9 @@ package ch.dissem.bitmessage.entity; import ch.dissem.bitmessage.entity.payload.*; -import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; +import ch.dissem.bitmessage.entity.valueobject.extended.Message; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.utils.TestBase; import ch.dissem.bitmessage.utils.TestUtils; @@ -31,7 +31,6 @@ import java.util.ArrayList; import java.util.Collections; import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; -import static ch.dissem.bitmessage.utils.Singleton.cryptography; import static org.junit.Assert.*; public class SerializationTest extends TestBase { @@ -104,7 +103,7 @@ public class SerializationTest extends TestBase { Plaintext p1 = new Plaintext.Builder(MSG) .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) .to(TestUtils.loadContact()) - .message(new ExtendedEncoding.Builder().message() + .message(new Message.Builder() .subject("Subject") .body("Message") .build()) @@ -154,7 +153,7 @@ public class SerializationTest extends TestBase { public void ensureNetworkMessageIsSerializedAndDeserializedCorrectly() throws Exception { ArrayList ivs = new ArrayList<>(50000); for (int i = 0; i < 50000; i++) { - ivs.add(new InventoryVector(cryptography().randomBytes(32))); + ivs.add(TestUtils.randomInventoryVector()); } Inv inv = new Inv.Builder().inventory(ivs).build(); diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/TestUtils.java b/core/src/test/java/ch/dissem/bitmessage/utils/TestUtils.java index 21f85ff..fffaba8 100644 --- a/core/src/test/java/ch/dissem/bitmessage/utils/TestUtils.java +++ b/core/src/test/java/ch/dissem/bitmessage/utils/TestUtils.java @@ -20,6 +20,7 @@ import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.payload.V4Pubkey; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.factory.Factory; @@ -28,6 +29,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Random; import static org.junit.Assert.assertEquals; @@ -35,6 +37,8 @@ import static org.junit.Assert.assertEquals; * If there's ever a need for this in production code, it should be rewritten to be more efficient. */ public class TestUtils { + public static final Random RANDOM = new Random(); + public static byte[] int16(int number) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); Encode.int16(number, out); @@ -59,6 +63,12 @@ public class TestUtils { return out.toByteArray(); } + public static InventoryVector randomInventoryVector() { + byte[] bytes = new byte[32]; + RANDOM.nextBytes(bytes); + return new InventoryVector(bytes); + } + public static InputStream getResource(String resourceName) { return TestUtils.class.getClassLoader().getResourceAsStream(resourceName); } diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java index 9d9b002..0d3feec 100644 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java @@ -26,6 +26,7 @@ import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.MessageRepository; +import ch.dissem.bitmessage.utils.TestUtils; import ch.dissem.bitmessage.utils.UnixTime; import org.junit.Before; import org.junit.Test; @@ -162,7 +163,7 @@ public class JdbcMessageRepositoryTest extends TestBase { @Test public void testSave() throws Exception { Plaintext message = new Plaintext.Builder(MSG) - .IV(new InventoryVector(cryptography().randomBytes(32))) + .IV(TestUtils.randomInventoryVector()) .from(identity) .to(contactA) .message("Subject", "Message") @@ -185,7 +186,7 @@ public class JdbcMessageRepositoryTest extends TestBase { public void testUpdate() throws Exception { List messages = repo.findMessages(Plaintext.Status.DRAFT, contactA); Plaintext message = messages.get(0); - message.setInventoryVector(new InventoryVector(cryptography().randomBytes(32))); + message.setInventoryVector(TestUtils.randomInventoryVector()); repo.save(message); messages = repo.findMessages(Plaintext.Status.DRAFT, contactA); @@ -206,7 +207,7 @@ public class JdbcMessageRepositoryTest extends TestBase { @Test public void ensureUnacknowledgedMessagesAreFoundForResend() throws Exception { Plaintext message = new Plaintext.Builder(MSG) - .IV(new InventoryVector(cryptography().randomBytes(32))) + .IV(TestUtils.randomInventoryVector()) .from(identity) .to(contactA) .message("Subject", "Message") @@ -240,4 +241,4 @@ public class JdbcMessageRepositoryTest extends TestBase { .build(); repo.save(message); } -} \ No newline at end of file +} From c11a1b78c4ae7187bdaafb16b0dc7b5b27acc4a5 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 11 Dec 2016 14:27:21 +0100 Subject: [PATCH 05/22] Some code style improvements --- .../bitmessage/entity/valueobject/extended/Message.java | 2 +- .../dissem/bitmessage/entity/valueobject/extended/Vote.java | 2 +- .../dissem/bitmessage/factory/ExtendedEncodingFactory.java | 3 ++- core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java | 5 ----- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java index 33e7b81..98c84bd 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java @@ -84,7 +84,7 @@ public class Message implements ExtendedEncoding.ExtendedType { } packer.packMapHeader(size); packer.packString(""); - packer.packString("message"); + packer.packString(TYPE); packer.packString("subject"); packer.packString(subject); packer.packString("body"); diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Vote.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Vote.java index 75cea67..d0beddd 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Vote.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Vote.java @@ -58,7 +58,7 @@ public class Vote implements ExtendedEncoding.ExtendedType { public void pack(MessagePacker packer) throws IOException { packer.packMapHeader(3); packer.packString(""); - packer.packString("vote"); + packer.packString(TYPE); packer.packString("msgId"); packer.packBinaryHeader(msgId.getHash().length); packer.writePayload(msgId.getHash()); diff --git a/core/src/main/java/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.java b/core/src/main/java/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.java index 0dc419c..29595da 100644 --- a/core/src/main/java/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.java +++ b/core/src/main/java/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.java @@ -22,6 +22,7 @@ import java.util.zip.InflaterInputStream; public class ExtendedEncodingFactory { private static final Logger LOG = LoggerFactory.getLogger(ExtendedEncodingFactory.class); private static final ExtendedEncodingFactory INSTANCE = new ExtendedEncodingFactory(); + private static final String KEY_MESSAGE_TYPE = ""; private Map<String, ExtendedEncoding.Unpacker<?>> factories = new HashMap<>(); private ExtendedEncodingFactory() { @@ -39,7 +40,7 @@ public class ExtendedEncodingFactory { MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(unzipper); int mapSize = unpacker.unpackMapHeader(); String key = unpacker.unpackString(); - if (!"".equals(key)) { + if (!KEY_MESSAGE_TYPE.equals(key)) { LOG.error("Unexpected content: " + key); return null; } diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java b/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java index c0bf554..986c288 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java @@ -16,11 +16,6 @@ package ch.dissem.bitmessage.utils; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.file.Files; - /** * A helper class for working with byte arrays interpreted as unsigned big endian integers. * This is one part due to the fact that Java doesn't support unsigned numbers, and another From 0d67701735381ec1b49ab1cf6d5ed68189a9bdd1 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Mon, 12 Dec 2016 07:59:38 +0100 Subject: [PATCH 06/22] Changed default CustomCommandHandler While throwing an IllegalStateException is great to signal developers they need to register a default handler, it's a dangerous default for all those who don't need one. A log entry on 'debug' level should suffice. --- .../src/main/java/ch/dissem/bitmessage/BitmessageContext.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java b/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java index 9e155ad..78bfa73 100644 --- a/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java +++ b/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java @@ -435,8 +435,8 @@ public class BitmessageContext { customCommandHandler = new CustomCommandHandler() { @Override public MessagePayload handle(CustomMessage request) { - throw new IllegalStateException( - "Received custom request, but no custom command handler configured."); + LOG.debug("Received custom request, but no custom command handler configured."); + return null; } }; } From 702ac6cb82d45a8fca1190ab5a1e22eef63b09d8 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 16 Dec 2016 07:30:02 +0100 Subject: [PATCH 07/22] Added some null checks so the users of this library don't have to --- .../entity/valueobject/extended/Message.java | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java index 98c84bd..b738d42 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java @@ -132,31 +132,44 @@ public class Message implements ExtendedEncoding.ExtendedType { } public Builder addParent(Plaintext parent) { - parents.add(parent.getInventoryVector()); + if (parent != null) { + InventoryVector iv = parent.getInventoryVector(); + if (iv == null) { + LOG.debug("Ignored parent without IV"); + } else { + parents.add(iv); + } + } return this; } public Builder addParent(InventoryVector iv) { - parents.add(iv); + if (iv != null) { + parents.add(iv); + } return this; } public Builder addFile(File file, Attachment.Disposition disposition) { - try { - files.add(new Attachment.Builder() - .name(file.getName()) - .disposition(disposition) - .type(URLConnection.guessContentTypeFromStream(new FileInputStream(file))) - .data(Files.readAllBytes(file.toPath())) - .build()); - } catch (IOException e) { - LOG.error(e.getMessage(), e); + if (file != null) { + try { + files.add(new Attachment.Builder() + .name(file.getName()) + .disposition(disposition) + .type(URLConnection.guessContentTypeFromStream(new FileInputStream(file))) + .data(Files.readAllBytes(file.toPath())) + .build()); + } catch (IOException e) { + LOG.error(e.getMessage(), e); + } } return this; } public Builder addFile(Attachment file) { - files.add(file); + if (file != null) { + files.add(file); + } return this; } From 732032b1b59d48307fd54114da0ec88738b2952c Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Wed, 21 Dec 2016 08:09:53 +0100 Subject: [PATCH 08/22] Fixed some bugs and some tests --- .../dissem/bitmessage/BitmessageContext.java | 5 +- .../dissem/bitmessage/entity/Plaintext.java | 12 ++ .../ports/AbstractMessageRepository.java | 25 ++- .../bitmessage/ports/AddressRepository.java | 5 + .../bitmessage/BitmessageContextTest.java | 162 ++++++++---------- .../java/ch/dissem/bitmessage/SystemTest.java | 51 +++--- .../repository/JdbcMessageRepository.java | 45 ++--- .../repository/JdbcNodeRegistry.java | 5 + 8 files changed, 170 insertions(+), 140 deletions(-) diff --git a/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java b/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java index 78bfa73..a36c2d8 100644 --- a/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java +++ b/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java @@ -254,7 +254,10 @@ public class BitmessageContext { public void addContact(BitmessageAddress contact) { ctx.getAddressRepository().save(contact); if (contact.getPubkey() == null) { - ctx.requestPubkey(contact); + BitmessageAddress stored = ctx.getAddressRepository().getAddress(contact.getAddress()); + if (stored.getPubkey() == null) { + ctx.requestPubkey(contact); + } } } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java index 06c1584..e46fd2a 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java @@ -377,6 +377,18 @@ public class Plaintext implements Streamable { return extendedData; } + @SuppressWarnings("unchecked") + public <T extends ExtendedEncoding.ExtendedType> T getExtendedData(Class<T> type) { + ExtendedEncoding extendedData = getExtendedData(); + if (extendedData == null) { + return null; + } + if (type == null || type.isInstance(extendedData.getContent())) { + return (T) extendedData.getContent(); + } + return null; + } + public List<InventoryVector> getParents() { if (Message.TYPE.equals(getExtendedData().getType())) { return ((Message) extendedData.getContent()).getParents(); diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/AbstractMessageRepository.java b/core/src/main/java/ch/dissem/bitmessage/ports/AbstractMessageRepository.java index e0448c7..5e51ace 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/AbstractMessageRepository.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/AbstractMessageRepository.java @@ -37,15 +37,28 @@ public abstract class AbstractMessageRepository implements MessageRepository, In this.ctx = context; } + /** + * @deprecated use {@link #saveContactIfNecessary(BitmessageAddress)} instead. + */ + @Deprecated protected void safeSenderIfNecessary(Plaintext message) { if (message.getId() == null) { - BitmessageAddress savedAddress = ctx.getAddressRepository().getAddress(message.getFrom().getAddress()); + saveContactIfNecessary(message.getFrom()); + } + } + + protected void saveContactIfNecessary(BitmessageAddress contact) { + if (contact != null) { + BitmessageAddress savedAddress = ctx.getAddressRepository().getAddress(contact.getAddress()); if (savedAddress == null) { - ctx.getAddressRepository().save(message.getFrom()); - } else if (savedAddress.getPubkey() == null && message.getFrom().getPubkey() != null) { - savedAddress.setPubkey(message.getFrom().getPubkey()); + ctx.getAddressRepository().save(contact); + } else if (savedAddress.getPubkey() == null && contact.getPubkey() != null) { + savedAddress.setPubkey(contact.getPubkey()); ctx.getAddressRepository().save(savedAddress); } + if (savedAddress != null) { + contact.setAlias(savedAddress.getAlias()); + } } } @@ -95,7 +108,7 @@ public abstract class AbstractMessageRepository implements MessageRepository, In @Override public List<Plaintext> findMessagesToResend() { return find("status='" + Plaintext.Status.SENT.name() + "'" + - " AND next_try < " + UnixTime.now()); + " AND next_try < " + UnixTime.now()); } @Override @@ -119,7 +132,7 @@ public abstract class AbstractMessageRepository implements MessageRepository, In return collection.iterator().next(); default: throw new ApplicationException("This shouldn't happen, found " + collection.size() + - " items, one or none was expected"); + " items, one or none was expected"); } } diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java b/core/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java index ae0a249..5247730 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java @@ -52,6 +52,11 @@ public interface AddressRepository { */ List<BitmessageAddress> getContacts(); + /** + * Implementations must not delete cryptographic keys if they're not provided by <code>address</code>. + * + * @param address to save or update + */ void save(BitmessageAddress address); void remove(BitmessageAddress address); diff --git a/core/src/test/java/ch/dissem/bitmessage/BitmessageContextTest.java b/core/src/test/java/ch/dissem/bitmessage/BitmessageContextTest.java index 813ff20..b4f536b 100644 --- a/core/src/test/java/ch/dissem/bitmessage/BitmessageContextTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/BitmessageContextTest.java @@ -57,64 +57,65 @@ public class BitmessageContextTest { Singleton.initialize(null); listener = mock(BitmessageContext.Listener.class); ctx = new BitmessageContext.Builder() - .addressRepo(mock(AddressRepository.class)) - .cryptography(new BouncyCryptography()) - .inventory(mock(Inventory.class)) - .listener(listener) - .messageRepo(mock(MessageRepository.class)) - .networkHandler(mock(NetworkHandler.class)) - .nodeRegistry(mock(NodeRegistry.class)) - .labeler(spy(new DefaultLabeler())) - .powRepo(spy(new ProofOfWorkRepository() { - Map<InventoryVector, Item> items = new HashMap<>(); + .addressRepo(mock(AddressRepository.class)) + .cryptography(new BouncyCryptography()) + .inventory(mock(Inventory.class)) + .listener(listener) + .messageRepo(mock(MessageRepository.class)) + .networkHandler(mock(NetworkHandler.class)) + .nodeRegistry(mock(NodeRegistry.class)) + .labeler(spy(new DefaultLabeler())) + .powRepo(spy(new ProofOfWorkRepository() { + Map<InventoryVector, Item> items = new HashMap<>(); - @Override - public Item getItem(byte[] initialHash) { - return items.get(new InventoryVector(initialHash)); - } + @Override + public Item getItem(byte[] initialHash) { + return items.get(new InventoryVector(initialHash)); + } - @Override - public List<byte[]> getItems() { - List<byte[]> result = new LinkedList<>(); - for (InventoryVector iv : items.keySet()) { - result.add(iv.getHash()); - } - return result; + @Override + public List<byte[]> getItems() { + List<byte[]> result = new LinkedList<>(); + for (InventoryVector iv : items.keySet()) { + result.add(iv.getHash()); } + return result; + } - @Override - public void putObject(Item item) { - items.put(new InventoryVector(cryptography().getInitialHash(item.object)), item); - } + @Override + public void putObject(Item item) { + items.put(new InventoryVector(cryptography().getInitialHash(item.object)), item); + } - @Override - public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { - items.put(new InventoryVector(cryptography().getInitialHash(object)), new Item(object, nonceTrialsPerByte, extraBytes)); - } + @Override + public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { + items.put(new InventoryVector(cryptography().getInitialHash(object)), new Item(object, nonceTrialsPerByte, extraBytes)); + } - @Override - public void removeObject(byte[] initialHash) { - items.remove(initialHash); - } - })) - .proofOfWorkEngine(spy(new ProofOfWorkEngine() { - @Override - public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) { - callback.onNonceCalculated(initialHash, new byte[8]); - } - })) - .build(); + @Override + public void removeObject(byte[] initialHash) { + items.remove(initialHash); + } + })) + .proofOfWorkEngine(spy(new ProofOfWorkEngine() { + @Override + public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) { + callback.onNonceCalculated(initialHash, new byte[8]); + } + })) + .build(); TTL.msg(2 * MINUTE); } @Test public void ensureContactIsSavedAndPubkeyRequested() { BitmessageAddress contact = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"); + when(ctx.addresses().getAddress(contact.getAddress())).thenReturn(contact); ctx.addContact(contact); - verify(ctx.addresses(), times(2)).save(contact); - verify(ctx.internals().getProofOfWorkEngine()) - .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); + verify(ctx.addresses(), timeout(1000).atLeastOnce()).save(contact); + verify(ctx.internals().getProofOfWorkEngine(), timeout(1000)) + .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); } @Test @@ -128,52 +129,41 @@ public class BitmessageContextTest { verify(ctx.addresses(), times(1)).save(contact); verify(ctx.internals().getProofOfWorkEngine(), never()) - .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); + .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); } @Test public void ensureV2PubkeyIsNotRequestedIfItExistsInInventory() throws Exception { BitmessageAddress contact = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"); when(ctx.internals().getInventory().getObjects(anyLong(), anyLong(), any(ObjectType.class))) - .thenReturn(Collections.singletonList( - TestUtils.loadObjectMessage(2, "V2Pubkey.payload") - )); + .thenReturn(Collections.singletonList( + TestUtils.loadObjectMessage(2, "V2Pubkey.payload") + )); + when(ctx.addresses().getAddress(contact.getAddress())).thenReturn(contact); ctx.addContact(contact); verify(ctx.addresses(), atLeastOnce()).save(contact); verify(ctx.internals().getProofOfWorkEngine(), never()) - .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); + .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); } @Test public void ensureV4PubkeyIsNotRequestedIfItExistsInInventory() throws Exception { BitmessageAddress contact = new BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h"); when(ctx.internals().getInventory().getObjects(anyLong(), anyLong(), any(ObjectType.class))) - .thenReturn(Collections.singletonList( - TestUtils.loadObjectMessage(2, "V4Pubkey.payload") - )); + .thenReturn(Collections.singletonList( + TestUtils.loadObjectMessage(2, "V4Pubkey.payload") + )); final BitmessageAddress stored = new BitmessageAddress(contact.getAddress()); stored.setAlias("Test"); when(ctx.addresses().getAddress(contact.getAddress())).thenReturn(stored); ctx.addContact(contact); - verify(ctx.addresses(), atLeastOnce()).save(argThat(new BaseMatcher<BitmessageAddress>() { - @Override - public boolean matches(Object item) { - return item instanceof BitmessageAddress - && ((BitmessageAddress) item).getPubkey() != null - && stored.getAlias().equals(((BitmessageAddress) item).getAlias()); - } - - @Override - public void describeTo(Description description) { - description.appendText("pubkey must not be null and alias must be ").appendValue(stored.getAlias()); - } - })); + verify(ctx.addresses(), atLeastOnce()).save(any(BitmessageAddress.class)); verify(ctx.internals().getProofOfWorkEngine(), never()) - .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); + .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); } @Test @@ -184,7 +174,7 @@ public class BitmessageContextTest { objects.add(TestUtils.loadObjectMessage(4, "V4Broadcast.payload")); objects.add(TestUtils.loadObjectMessage(5, "V5Broadcast.payload")); when(ctx.internals().getInventory().getObjects(eq(address.getStream()), anyLong(), any(ObjectType.class))) - .thenReturn(objects); + .thenReturn(objects); when(ctx.addresses().getSubscriptions(anyLong())).thenReturn(Collections.singletonList(address)); ctx.addSubscribtion(address); @@ -203,48 +193,48 @@ public class BitmessageContextTest { @Test public void ensureMessageIsSent() throws Exception { ctx.send(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), TestUtils.loadContact(), - "Subject", "Message"); + "Subject", "Message"); assertEquals(2, ctx.internals().getProofOfWorkRepository().getItems().size()); verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce()) - .putObject(object(MSG), eq(1000L), eq(1000L)); + .putObject(object(MSG), eq(1000L), eq(1000L)); verify(ctx.messages(), timeout(10000).atLeastOnce()).save(MessageMatchers.plaintext(Type.MSG)); } @Test public void ensurePubkeyIsRequestedIfItIsMissing() throws Exception { ctx.send(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), - new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"), - "Subject", "Message"); + new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"), + "Subject", "Message"); verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce()) - .putObject(object(GET_PUBKEY), eq(1000L), eq(1000L)); + .putObject(object(GET_PUBKEY), eq(1000L), eq(1000L)); verify(ctx.messages(), timeout(10000).atLeastOnce()).save(MessageMatchers.plaintext(Type.MSG)); } @Test(expected = IllegalArgumentException.class) public void ensureSenderMustBeIdentity() { ctx.send(new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"), - new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"), - "Subject", "Message"); + new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"), + "Subject", "Message"); } @Test public void ensureBroadcastIsSent() throws Exception { ctx.broadcast(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), - "Subject", "Message"); + "Subject", "Message"); verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce()) - .putObject(object(BROADCAST), eq(1000L), eq(1000L)); + .putObject(object(BROADCAST), eq(1000L), eq(1000L)); verify(ctx.internals().getProofOfWorkEngine()) - .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); + .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); verify(ctx.messages(), timeout(10000).atLeastOnce()) - .save(MessageMatchers.plaintext(Type.BROADCAST)); + .save(MessageMatchers.plaintext(Type.BROADCAST)); } @Test(expected = IllegalArgumentException.class) public void ensureSenderWithoutPrivateKeyThrowsException() { Plaintext msg = new Plaintext.Builder(Type.BROADCAST) - .from(new BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) - .message("Subject", "Message") - .build(); + .from(new BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) + .message("Subject", "Message") + .build(); ctx.send(msg); } @@ -301,11 +291,11 @@ public class BitmessageContextTest { @Test public void ensureUnacknowledgedMessageIsResent() throws Exception { Plaintext plaintext = new Plaintext.Builder(Type.MSG) - .ttl(1) - .message("subject", "message") - .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) - .to(TestUtils.loadContact()) - .build(); + .ttl(1) + .message("subject", "message") + .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) + .to(TestUtils.loadContact()) + .build(); assertTrue(plaintext.getTo().has(Pubkey.Feature.DOES_ACK)); when(ctx.messages().findMessagesToResend()).thenReturn(Collections.singletonList(plaintext)); when(ctx.messages().getMessage(any(byte[].class))).thenReturn(plaintext); diff --git a/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java b/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java index dac6f74..506045f 100644 --- a/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java +++ b/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java @@ -54,10 +54,11 @@ public class SystemTest { } @Parameterized.Parameters + @SuppressWarnings("deprecation") public static List<Object[]> parameters() { return Arrays.asList(new Object[][]{ - {new NioNetworkHandler(), new DefaultNetworkHandler()}, - {new NioNetworkHandler(), new NioNetworkHandler()} + {new NioNetworkHandler(), new DefaultNetworkHandler()}, + {new NioNetworkHandler(), new NioNetworkHandler()} }); } @@ -70,33 +71,33 @@ public class SystemTest { TTL.pubkey(5 * MINUTE); JdbcConfig aliceDB = new JdbcConfig("jdbc:h2:mem:alice;DB_CLOSE_DELAY=-1", "sa", ""); alice = new BitmessageContext.Builder() - .addressRepo(new JdbcAddressRepository(aliceDB)) - .inventory(new JdbcInventory(aliceDB)) - .messageRepo(new JdbcMessageRepository(aliceDB)) - .powRepo(new JdbcProofOfWorkRepository(aliceDB)) - .port(alicePort) - .nodeRegistry(new TestNodeRegistry(bobPort)) - .networkHandler(aliceNetworkHandler) - .cryptography(new BouncyCryptography()) - .listener(aliceListener) - .labeler(aliceLabeler) - .build(); + .addressRepo(new JdbcAddressRepository(aliceDB)) + .inventory(new JdbcInventory(aliceDB)) + .messageRepo(new JdbcMessageRepository(aliceDB)) + .powRepo(new JdbcProofOfWorkRepository(aliceDB)) + .port(alicePort) + .nodeRegistry(new TestNodeRegistry(bobPort)) + .networkHandler(aliceNetworkHandler) + .cryptography(new BouncyCryptography()) + .listener(aliceListener) + .labeler(aliceLabeler) + .build(); alice.startup(); aliceIdentity = alice.createIdentity(false, DOES_ACK); JdbcConfig bobDB = new JdbcConfig("jdbc:h2:mem:bob;DB_CLOSE_DELAY=-1", "sa", ""); bob = new BitmessageContext.Builder() - .addressRepo(new JdbcAddressRepository(bobDB)) - .inventory(new JdbcInventory(bobDB)) - .messageRepo(new JdbcMessageRepository(bobDB)) - .powRepo(new JdbcProofOfWorkRepository(bobDB)) - .port(bobPort) - .nodeRegistry(new TestNodeRegistry(alicePort)) - .networkHandler(bobNetworkHandler) - .cryptography(new BouncyCryptography()) - .listener(bobListener) - .labeler(new DebugLabeler("Bob")) - .build(); + .addressRepo(new JdbcAddressRepository(bobDB)) + .inventory(new JdbcInventory(bobDB)) + .messageRepo(new JdbcMessageRepository(bobDB)) + .powRepo(new JdbcProofOfWorkRepository(bobDB)) + .port(bobPort) + .nodeRegistry(new TestNodeRegistry(alicePort)) + .networkHandler(bobNetworkHandler) + .cryptography(new BouncyCryptography()) + .listener(bobListener) + .labeler(new DebugLabeler("Bob")) + .build(); bob.startup(); bobIdentity = bob.createIdentity(false, DOES_ACK); @@ -121,7 +122,7 @@ public class SystemTest { assertThat(plaintext.getText(), equalTo(originalMessage)); Mockito.verify(aliceLabeler, Mockito.timeout(TimeUnit.MINUTES.toMillis(15)).atLeastOnce()) - .markAsAcknowledged(any()); + .markAsAcknowledged(any()); } @Test(timeout = 30_000) diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java index 3788d96..9851eb4 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java @@ -46,7 +46,7 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements @Override protected List<Label> findLabels(String where) { try ( - Connection connection = config.getConnection() + Connection connection = config.getConnection() ) { return findLabels(connection, where); } catch (SQLException e) { @@ -76,12 +76,12 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ") AND "; } where += "id IN (SELECT message_id FROM Message_Label WHERE label_id IN (" + - "SELECT id FROM Label WHERE type = '" + Label.Type.UNREAD.name() + "'))"; + "SELECT id FROM Label WHERE type = '" + Label.Type.UNREAD.name() + "'))"; try ( - Connection connection = config.getConnection(); - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT count(*) FROM Message WHERE " + where) + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT count(*) FROM Message WHERE " + where) ) { if (rs.next()) { return rs.getInt(1); @@ -96,11 +96,11 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements protected List<Plaintext> find(String where) { List<Plaintext> result = new LinkedList<>(); try ( - Connection connection = config.getConnection(); - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery( - "SELECT id, iv, type, sender, recipient, data, ack_data, sent, received, initial_hash, status, ttl, retries, next_try " + - "FROM Message WHERE " + where) + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT id, iv, type, sender, recipient, data, ack_data, sent, received, initial_hash, status, ttl, retries, next_try " + + "FROM Message WHERE " + where) ) { while (rs.next()) { byte[] iv = rs.getBytes("iv"); @@ -120,7 +120,7 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements builder.retries(rs.getInt("retries")); builder.nextTry(rs.getLong("next_try")); builder.labels(findLabels(connection, - "id IN (SELECT label_id FROM Message_Label WHERE message_id=" + id + ") ORDER BY ord")); + "id IN (SELECT label_id FROM Message_Label WHERE message_id=" + id + ") ORDER BY ord")); Plaintext message = builder.build(); message.setInitialHash(rs.getBytes("initial_hash")); result.add(message); @@ -134,8 +134,8 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements private List<Label> findLabels(Connection connection, String where) { List<Label> result = new ArrayList<>(); try ( - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT id, label, type, color FROM Label WHERE " + where) + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT id, label, type, color FROM Label WHERE " + where) ) { while (rs.next()) { result.add(getLabel(rs)); @@ -148,7 +148,8 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements @Override public void save(Plaintext message) { - safeSenderIfNecessary(message); + saveContactIfNecessary(message.getFrom()); + saveContactIfNecessary(message.getTo()); try (Connection connection = config.getConnection()) { try { @@ -180,7 +181,7 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements } // save new labels try (PreparedStatement ps = connection.prepareStatement("INSERT INTO Message_Label VALUES (" + - message.getId() + ", ?)")) { + message.getId() + ", ?)")) { for (Label label : message.getLabels()) { ps.setLong(1, (Long) label.getId()); ps.executeUpdate(); @@ -190,10 +191,10 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements private void insert(Connection connection, Plaintext message) throws SQLException, IOException { try (PreparedStatement ps = connection.prepareStatement( - "INSERT INTO Message (iv, type, sender, recipient, data, ack_data, sent, received, " + - "status, initial_hash, ttl, retries, next_try) " + - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - Statement.RETURN_GENERATED_KEYS) + "INSERT INTO Message (iv, type, sender, recipient, data, ack_data, sent, received, " + + "status, initial_hash, ttl, retries, next_try) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + Statement.RETURN_GENERATED_KEYS) ) { ps.setBytes(1, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash()); ps.setString(2, message.getType().name()); @@ -220,9 +221,9 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements private void update(Connection connection, Plaintext message) throws SQLException, IOException { try (PreparedStatement ps = connection.prepareStatement( - "UPDATE Message SET iv=?, type=?, sender=?, recipient=?, data=?, ack_data=?, sent=?, received=?, " + - "status=?, initial_hash=?, ttl=?, retries=?, next_try=? " + - "WHERE id=?")) { + "UPDATE Message SET iv=?, type=?, sender=?, recipient=?, data=?, ack_data=?, sent=?, received=?, " + + "status=?, initial_hash=?, ttl=?, retries=?, next_try=? " + + "WHERE id=?")) { ps.setBytes(1, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash()); ps.setString(2, message.getType().name()); ps.setString(3, message.getFrom().getAddress()); diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcNodeRegistry.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcNodeRegistry.java index 07d343a..8dd006f 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcNodeRegistry.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcNodeRegistry.java @@ -109,6 +109,11 @@ public class JdbcNodeRegistry extends JdbcHelper implements NodeRegistry { result.add(Collections.selectRandom(nodes)); } } + if (result.isEmpty()) { + // There might have been an error resolving domain names due to a missing internet exception. + // Try to load the stable nodes again next time. + stableNodes = null; + } } return result; } From 3f8980e236a08c1b4263ee743523cbb4e691ea97 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Wed, 11 Jan 2017 17:30:04 +0100 Subject: [PATCH 09/22] Make sure context is set after it is properly initialized --- .../main/java/ch/dissem/bitmessage/BitmessageContext.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java b/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java index a36c2d8..8d3c0d7 100644 --- a/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java +++ b/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java @@ -66,13 +66,13 @@ public class BitmessageContext { private final boolean sendPubkeyOnIdentityCreation; private BitmessageContext(Builder builder) { - if (builder.listener instanceof Listener.WithContext) { - ((Listener.WithContext) builder.listener).setContext(this); - } ctx = new InternalContext(builder); labeler = builder.labeler; ctx.getProofOfWorkService().doMissingProofOfWork(30_000); // TODO: this should be configurable sendPubkeyOnIdentityCreation = builder.sendPubkeyOnIdentityCreation; + if (builder.listener instanceof Listener.WithContext) { + ((Listener.WithContext) builder.listener).setContext(this); + } } public AddressRepository addresses() { From 10a45cc79c40646499effdc53268066bfd621a90 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 3 Feb 2017 07:29:51 +0100 Subject: [PATCH 10/22] Moved to own MsgPack implementation --- build.gradle | 1 + core/build.gradle | 2 +- .../entity/valueobject/ExtendedEncoding.java | 21 +-- .../entity/valueobject/extended/Message.java | 161 ++++++------------ .../entity/valueobject/extended/Vote.java | 49 ++---- .../factory/ExtendedEncodingFactory.java | 28 +-- .../ch/dissem/bitmessage/utils/Strings.java | 4 + .../dissem/bitmessage/demo/Application.java | 34 +++- 8 files changed, 129 insertions(+), 171 deletions(-) diff --git a/build.gradle b/build.gradle index 311c3f9..13b3ad0 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,7 @@ subprojects { repositories { mavenCentral() + maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } } test { diff --git a/core/build.gradle b/core/build.gradle index 1c2d07f..2fe6d50 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -25,7 +25,7 @@ artifacts { dependencies { compile 'org.slf4j:slf4j-api:1.7.12' - compile 'org.msgpack:msgpack-core:0.8.11' + compile 'ch.dissem.msgpack:msgpack:development-SNAPSHOT' testCompile 'junit:junit:4.12' testCompile 'org.hamcrest:hamcrest-library:1.3' testCompile 'org.mockito:mockito-core:1.10.19' diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java index f5a15c7..3f3d1ec 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/ExtendedEncoding.java @@ -1,9 +1,9 @@ package ch.dissem.bitmessage.entity.valueobject; import ch.dissem.bitmessage.exception.ApplicationException; -import org.msgpack.core.MessagePack; -import org.msgpack.core.MessagePacker; -import org.msgpack.core.MessageUnpacker; +import ch.dissem.msgpack.types.MPMap; +import ch.dissem.msgpack.types.MPString; +import ch.dissem.msgpack.types.MPType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,13 +39,10 @@ public class ExtendedEncoding implements Serializable { } public byte[] zip() { - try (ByteArrayOutputStream out = new ByteArrayOutputStream(); - DeflaterOutputStream zipper = new DeflaterOutputStream(out)) { - - MessagePacker packer = MessagePack.newDefaultPacker(zipper); - content.pack(packer); - packer.close(); - zipper.close(); + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + try (DeflaterOutputStream zipper = new DeflaterOutputStream(out)) { + content.pack().pack(zipper); + } return out.toByteArray(); } catch (IOException e) { throw new ApplicationException(e); @@ -68,12 +65,12 @@ public class ExtendedEncoding implements Serializable { public interface Unpacker<T extends ExtendedType> { String getType(); - T unpack(MessageUnpacker unpacker, int size); + T unpack(MPMap<MPString, MPType<?>> map); } public interface ExtendedType extends Serializable { String getType(); - void pack(MessagePacker packer) throws IOException; + MPMap<MPString, MPType<?>> pack() throws IOException; } } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java index b738d42..5bba98b 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Message.java @@ -3,8 +3,7 @@ package ch.dissem.bitmessage.entity.valueobject.extended; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import org.msgpack.core.MessagePacker; -import org.msgpack.core.MessageUnpacker; +import ch.dissem.msgpack.types.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,6 +14,10 @@ import java.net.URLConnection; import java.nio.file.Files; import java.util.*; +import static ch.dissem.bitmessage.entity.valueobject.extended.Attachment.Disposition.attachment; +import static ch.dissem.bitmessage.utils.Strings.str; +import static ch.dissem.msgpack.types.Utils.mp; + /** * Extended encoding type 'message'. Properties 'parents' and 'files' not yet supported by PyBitmessage, so they might not work * properly with future PyBitmessage implementations. @@ -74,45 +77,33 @@ public class Message implements ExtendedEncoding.ExtendedType { return Objects.hash(subject, body, parents, files); } - public void pack(MessagePacker packer) throws IOException { - int size = 3; + @Override + public MPMap<MPString, MPType<?>> pack() throws IOException { + MPMap<MPString, MPType<?>> result = new MPMap<>(); + result.put(mp(""), mp(TYPE)); + result.put(mp("subject"), mp(subject)); + result.put(mp("body"), mp(body)); + if (!files.isEmpty()) { - size++; - } - if (!parents.isEmpty()) { - size++; - } - packer.packMapHeader(size); - packer.packString(""); - packer.packString(TYPE); - packer.packString("subject"); - packer.packString(subject); - packer.packString("body"); - packer.packString(body); - if (!files.isEmpty()) { - packer.packString("files"); - packer.packArrayHeader(files.size()); + MPArray<MPMap<MPString, MPType<?>>> items = new MPArray<>(); + result.put(mp("files"), items); for (Attachment file : files) { - packer.packMapHeader(4); - packer.packString("name"); - packer.packString(file.getName()); - packer.packString("data"); - packer.packBinaryHeader(file.getData().length); - packer.writePayload(file.getData()); - packer.packString("type"); - packer.packString(file.getType()); - packer.packString("disposition"); - packer.packString(file.getDisposition().name()); + MPMap<MPString, MPType<?>> item = new MPMap<>(); + item.put(mp("name"), mp(file.getName())); + item.put(mp("data"), mp(file.getData())); + item.put(mp("type"), mp(file.getType())); + item.put(mp("disposition"), mp(file.getDisposition().name())); + items.add(item); } } if (!parents.isEmpty()) { - packer.packString("parents"); - packer.packArrayHeader(parents.size()); + MPArray<MPBinary> items = new MPArray<>(); + result.put(mp("parents"), items); for (InventoryVector parent : parents) { - packer.packBinaryHeader(parent.getHash().length); - packer.writePayload(parent.getHash()); + items.add(mp(parent.getHash())); } } + return result; } public static class Builder { @@ -185,86 +176,44 @@ public class Message implements ExtendedEncoding.ExtendedType { } @Override - public Message unpack(MessageUnpacker unpacker, int size) { + public Message unpack(MPMap<MPString, MPType<?>> map) { Message.Builder builder = new Message.Builder(); - try { - for (int i = 0; i < size; i++) { - String key = unpacker.unpackString(); - switch (key) { - case "subject": - builder.subject(unpacker.unpackString()); - break; - case "body": - builder.body(unpacker.unpackString()); - break; - case "parents": - builder.parents = unpackParents(unpacker); - break; - case "files": - builder.files = unpackFiles(unpacker); - break; - default: - LOG.error("Unexpected data with key: " + key); - break; - } + builder.subject(str(map.get(mp("subject")))); + builder.body(str(map.get(mp("body")))); + @SuppressWarnings("unchecked") + MPArray<MPBinary> parents = (MPArray<MPBinary>) map.get(mp("parents")); + if (parents != null) { + for (MPBinary parent : parents) { + builder.addParent(new InventoryVector(parent.getValue())); } - } catch (IOException e) { - LOG.error(e.getMessage(), e); } + @SuppressWarnings("unchecked") + MPArray<MPMap<MPString, MPType<?>>> files = (MPArray<MPMap<MPString, MPType<?>>>) map.get(mp("files")); + if (files != null) { + for (MPMap<MPString, MPType<?>> item : files) { + Attachment.Builder b = new Attachment.Builder(); + b.name(str(item.get(mp("name")))); + b.data(bin(item.get(mp("data")))); + b.type(str(item.get(mp("type")))); + String disposition = str(item.get(mp("disposition"))); + if ("inline".equals(disposition)) { + b.inline(); + } else if ("attachment".equals(disposition)) { + b.attachment(); + } + builder.addFile(b.build()); + } + } + return new Message(builder); } - private static List<InventoryVector> unpackParents(MessageUnpacker unpacker) throws IOException { - int size = unpacker.unpackArrayHeader(); - List<InventoryVector> parents = new ArrayList<>(size); - for (int i = 0; i < size; i++) { - int binarySize = unpacker.unpackBinaryHeader(); - parents.add(new InventoryVector(unpacker.readPayload(binarySize))); + private byte[] bin(MPType data) { + if (data instanceof MPBinary) { + return ((MPBinary) data).getValue(); + } else { + return null; } - return parents; - } - - private static List<Attachment> unpackFiles(MessageUnpacker unpacker) throws IOException { - int size = unpacker.unpackArrayHeader(); - List<Attachment> files = new ArrayList<>(size); - for (int i = 0; i < size; i++) { - Attachment.Builder attachment = new Attachment.Builder(); - int mapSize = unpacker.unpackMapHeader(); - for (int j = 0; j < mapSize; j++) { - String key = unpacker.unpackString(); - switch (key) { - case "name": - attachment.name(unpacker.unpackString()); - break; - case "data": - int binarySize = unpacker.unpackBinaryHeader(); - attachment.data(unpacker.readPayload(binarySize)); - break; - case "type": - attachment.type(unpacker.unpackString()); - break; - case "disposition": - String disposition = unpacker.unpackString(); - switch (disposition) { - case "inline": - attachment.inline(); - break; - case "attachment": - attachment.attachment(); - break; - default: - LOG.debug("Unknown disposition: " + disposition); - break; - } - break; - default: - LOG.debug("Unknown file info '" + key + "' with data: " + unpacker.unpackValue()); - break; - } - } - files.add(attachment.build()); - } - return files; } } } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Vote.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Vote.java index d0beddd..6ea174f 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Vote.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/extended/Vote.java @@ -3,20 +3,19 @@ package ch.dissem.bitmessage.entity.valueobject.extended; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import org.msgpack.core.MessagePacker; -import org.msgpack.core.MessageUnpacker; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import ch.dissem.msgpack.types.*; import java.io.IOException; import java.util.Objects; +import static ch.dissem.bitmessage.utils.Strings.str; +import static ch.dissem.msgpack.types.Utils.mp; + /** * Extended encoding type 'vote'. Specification still outstanding, so this will need some work. */ public class Vote implements ExtendedEncoding.ExtendedType { private static final long serialVersionUID = -8427038604209964837L; - private static final Logger LOG = LoggerFactory.getLogger(Vote.class); public static final String TYPE = "vote"; @@ -55,15 +54,13 @@ public class Vote implements ExtendedEncoding.ExtendedType { return Objects.hash(msgId, vote); } - public void pack(MessagePacker packer) throws IOException { - packer.packMapHeader(3); - packer.packString(""); - packer.packString(TYPE); - packer.packString("msgId"); - packer.packBinaryHeader(msgId.getHash().length); - packer.writePayload(msgId.getHash()); - packer.packString("vote"); - packer.packString(vote); + @Override + public MPMap<MPString, MPType<?>> pack() throws IOException { + MPMap<MPString, MPType<?>> result = new MPMap<>(); + result.put(mp(""), mp(TYPE)); + result.put(mp("msgId"), mp(msgId.getHash())); + result.put(mp("vote"), mp(vote)); + return result; } public static class Builder { @@ -104,27 +101,13 @@ public class Vote implements ExtendedEncoding.ExtendedType { } @Override - public Vote unpack(MessageUnpacker unpacker, int size) { + public Vote unpack(MPMap<MPString, MPType<?>> map) { Vote.Builder builder = new Vote.Builder(); - try { - for (int i = 0; i < size; i++) { - String key = unpacker.unpackString(); - switch (key) { - case "msgId": - int binarySize = unpacker.unpackBinaryHeader(); - builder.msgId(new InventoryVector(unpacker.readPayload(binarySize))); - break; - case "vote": - builder.vote(unpacker.unpackString()); - break; - default: - LOG.error("Unexpected data with key: " + key); - break; - } - } - } catch (IOException e) { - LOG.error(e.getMessage(), e); + MPType<?> msgId = map.get(mp("msgId")); + if (msgId instanceof MPBinary) { + builder.msgId(new InventoryVector(((MPBinary) msgId).getValue())); } + builder.vote(str(map.get(mp("vote")))); return new Vote(builder); } } diff --git a/core/src/main/java/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.java b/core/src/main/java/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.java index 29595da..b94b5f9 100644 --- a/core/src/main/java/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.java +++ b/core/src/main/java/ch/dissem/bitmessage/factory/ExtendedEncodingFactory.java @@ -4,8 +4,10 @@ import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; import ch.dissem.bitmessage.entity.valueobject.extended.Message; import ch.dissem.bitmessage.entity.valueobject.extended.Vote; import ch.dissem.bitmessage.exception.ApplicationException; -import org.msgpack.core.MessagePack; -import org.msgpack.core.MessageUnpacker; +import ch.dissem.msgpack.Reader; +import ch.dissem.msgpack.types.MPMap; +import ch.dissem.msgpack.types.MPString; +import ch.dissem.msgpack.types.MPType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,6 +17,8 @@ import java.util.HashMap; import java.util.Map; import java.util.zip.InflaterInputStream; +import static ch.dissem.bitmessage.utils.Strings.str; + /** * Factory that creates {@link ExtendedEncoding} objects from byte arrays. You can register your own types by adding a * {@link ExtendedEncoding.Unpacker} using {@link #registerFactory(ExtendedEncoding.Unpacker)}. @@ -22,7 +26,7 @@ import java.util.zip.InflaterInputStream; public class ExtendedEncodingFactory { private static final Logger LOG = LoggerFactory.getLogger(ExtendedEncodingFactory.class); private static final ExtendedEncodingFactory INSTANCE = new ExtendedEncodingFactory(); - private static final String KEY_MESSAGE_TYPE = ""; + private static final MPString KEY_MESSAGE_TYPE = new MPString(""); private Map<String, ExtendedEncoding.Unpacker<?>> factories = new HashMap<>(); private ExtendedEncodingFactory() { @@ -37,17 +41,17 @@ public class ExtendedEncodingFactory { public ExtendedEncoding unzip(byte[] zippedData) { try (InflaterInputStream unzipper = new InflaterInputStream(new ByteArrayInputStream(zippedData))) { - MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(unzipper); - int mapSize = unpacker.unpackMapHeader(); - String key = unpacker.unpackString(); - if (!KEY_MESSAGE_TYPE.equals(key)) { - LOG.error("Unexpected content: " + key); + Reader reader = Reader.getInstance(); + @SuppressWarnings("unchecked") + MPMap<MPString, MPType<?>> map = (MPMap<MPString, MPType<?>>) reader.read(unzipper); + MPType<?> messageType = map.get(KEY_MESSAGE_TYPE); + if (messageType == null) { + LOG.error("Missing message type"); return null; } - String type = unpacker.unpackString(); - ExtendedEncoding.Unpacker<?> factory = factories.get(type); - return new ExtendedEncoding(factory.unpack(unpacker, mapSize - 1)); - } catch (IOException e) { + ExtendedEncoding.Unpacker<?> factory = factories.get(str(messageType)); + return new ExtendedEncoding(factory.unpack(map)); + } catch (ClassCastException | IOException e) { throw new ApplicationException(e); } } diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Strings.java b/core/src/main/java/ch/dissem/bitmessage/utils/Strings.java index 7c9e13f..96383f7 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Strings.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Strings.java @@ -37,4 +37,8 @@ public class Strings { } return hex; } + + public static String str(Object o) { + return o == null ? null : o.toString(); + } } diff --git a/demo/src/main/java/ch/dissem/bitmessage/demo/Application.java b/demo/src/main/java/ch/dissem/bitmessage/demo/Application.java index 3861831..d915215 100644 --- a/demo/src/main/java/ch/dissem/bitmessage/demo/Application.java +++ b/demo/src/main/java/ch/dissem/bitmessage/demo/Application.java @@ -21,22 +21,26 @@ import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.valueobject.Label; +import ch.dissem.bitmessage.entity.valueobject.extended.Message; import org.apache.commons.lang3.text.WordUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.InetAddress; import java.util.List; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static ch.dissem.bitmessage.demo.CommandLine.COMMAND_BACK; import static ch.dissem.bitmessage.demo.CommandLine.ERROR_UNKNOWN_COMMAND; +import static java.util.regex.Pattern.CASE_INSENSITIVE; /** * A simple command line Bitmessage application */ public class Application { private final static Logger LOG = LoggerFactory.getLogger(Application.class); + private final static Pattern RESPONSE_PATTERN = Pattern.compile("^RE:.*$", CASE_INSENSITIVE); private final CommandLine commandLine; private BitmessageContext ctx; @@ -342,7 +346,7 @@ public class Application { System.out.println(); System.out.println("c) compose message"); System.out.println("s) compose broadcast"); - if (label.getType() == Label.Type.TRASH) { + if (label != null && label.getType() == Label.Type.TRASH) { System.out.println("e) empty trash"); } System.out.println(COMMAND_BACK); @@ -392,7 +396,7 @@ public class Application { command = commandLine.nextCommand(); switch (command) { case "r": - compose(message.getTo(), message.getFrom(), "RE: " + message.getSubject()); + compose(message.getTo(), message.getFrom(), message); break; case "d": ctx.labeler().delete(message); @@ -442,14 +446,20 @@ public class Application { return commandLine.selectAddress(addresses, "To:"); } - private void compose(BitmessageAddress from, BitmessageAddress to, String subject) { + private void compose(BitmessageAddress from, BitmessageAddress to, Plaintext parent) { boolean broadcast = (to == null); + String subject; System.out.println(); System.out.println("From: " + from); if (!broadcast) { System.out.println("To: " + to); } - if (subject != null) { + if (parent != null) { + if (RESPONSE_PATTERN.matcher(parent.getSubject()).matches()) { + subject = parent.getSubject(); + } else { + subject = "RE: " + parent.getSubject(); + } System.out.println("Subject: " + subject); } else { System.out.print("Subject: "); @@ -462,10 +472,20 @@ public class Application { line = commandLine.nextLine(); message.append(line).append('\n'); } while (line.length() > 0 || !commandLine.yesNo("Send message?")); - if (broadcast) { - ctx.broadcast(from, subject, message.toString()); + Plaintext.Type type = broadcast ? Plaintext.Type.BROADCAST : Plaintext.Type.MSG; + Plaintext.Builder builder = new Plaintext.Builder(type); + builder.from(from); + builder.to(to); + if (commandLine.yesNo("Use extended encoding?")) { + Message.Builder extended = new Message.Builder(); + extended.subject(subject).body(message.toString()); + if (parent != null) { + extended.addParent(parent); + } + builder.message(extended.build()); } else { - ctx.send(from, to, subject, message.toString()); + builder.message(subject, message.toString()); } + ctx.send(builder.build()); } } From d9090eb70cf75938a2483e5ab5295d10037b3dd6 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Mon, 13 Mar 2017 22:49:40 +0100 Subject: [PATCH 11/22] Some code to work with conversations --- README.md | 2 +- core/build.gradle | 2 +- .../bitmessage/DefaultMessageListener.java | 2 +- .../dissem/bitmessage/entity/Plaintext.java | 37 ++++- .../ports/AbstractMessageRepository.java | 22 +++ .../bitmessage/ports/MessageRepository.java | 23 +++ .../dissem/bitmessage/ports/NodeRegistry.java | 6 + .../bitmessage/utils/ConversationService.java | 112 +++++++++++++ .../ch/dissem/bitmessage/utils/Decode.java | 2 +- .../utils/ConversationServiceTest.java | 126 +++++++++++++++ .../dissem/bitmessage/TestNodeRegistry.java | 5 + .../repository/JdbcMessageRepository.java | 85 +++++++++- .../repository/JdbcNodeRegistry.java | 13 ++ .../V4.0__Create_table_message_parent.sql | 11 ++ .../repository/JdbcMessageRepositoryTest.java | 148 +++++++++++++++--- .../bitmessage/repository/TestJdbcConfig.java | 16 ++ 16 files changed, 573 insertions(+), 39 deletions(-) create mode 100644 core/src/main/java/ch/dissem/bitmessage/utils/ConversationService.java create mode 100644 core/src/test/java/ch/dissem/bitmessage/utils/ConversationServiceTest.java create mode 100644 repositories/src/main/resources/db/migration/V4.0__Create_table_message_parent.sql diff --git a/README.md b/README.md index 0f536d9..75f2b28 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A Java implementation for the Bitmessage protocol. To build, use command `./grad Please note that it still has its limitations, but the API should now be stable. Jabit uses Semantic Versioning, meaning as long as the major version doesn't change, nothing should break if you update. -Be aware though that this doesn't necessarily applies for SNAPSHOT builds and the development branch, notably when it comes to database updates. _In other words, they may break your installation!_ +Be aware though that this doesn't necessarily applies for SNAPSHOT builds and the development branch, notably when it comes to database updates. In other words, they may break your installation!_ #### Master [![Build Status](https://travis-ci.org/Dissem/Jabit.svg?branch=master)](https://travis-ci.org/Dissem/Jabit) diff --git a/core/build.gradle b/core/build.gradle index 2fe6d50..2fb78cd 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -25,7 +25,7 @@ artifacts { dependencies { compile 'org.slf4j:slf4j-api:1.7.12' - compile 'ch.dissem.msgpack:msgpack:development-SNAPSHOT' + compile 'ch.dissem.msgpack:msgpack:1.0.0' testCompile 'junit:junit:4.12' testCompile 'org.hamcrest:hamcrest-library:1.3' testCompile 'org.mockito:mockito-core:1.10.19' diff --git a/core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java b/core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java index b61f747..bdb61b6 100644 --- a/core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java +++ b/core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java @@ -87,7 +87,7 @@ class DefaultMessageListener implements NetworkHandler.MessageListener, Internal BitmessageAddress identity = ctx.getAddressRepository().findIdentity(getPubkey.getRipeTag()); if (identity != null && identity.getPrivateKey() != null && !identity.isChan()) { LOG.info("Got pubkey request for identity " + identity); - // FIXME: only send pubkey if it wasn't sent in the last 28 days + // FIXME: only send pubkey if it wasn't sent in the last TTL.pubkey() days ctx.sendPubkey(identity, object.getStream()); } } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java index e46fd2a..dad561d 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java @@ -18,22 +18,20 @@ package ch.dissem.bitmessage.entity; import ch.dissem.bitmessage.entity.payload.Msg; import ch.dissem.bitmessage.entity.payload.Pubkey.Feature; -import ch.dissem.bitmessage.entity.valueobject.extended.Attachment; import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; +import ch.dissem.bitmessage.entity.valueobject.extended.Attachment; import ch.dissem.bitmessage.entity.valueobject.extended.Message; import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.factory.ExtendedEncodingFactory; import ch.dissem.bitmessage.factory.Factory; -import ch.dissem.bitmessage.utils.Decode; -import ch.dissem.bitmessage.utils.Encode; -import ch.dissem.bitmessage.utils.TTL; -import ch.dissem.bitmessage.utils.UnixTime; +import ch.dissem.bitmessage.utils.*; import java.io.*; import java.nio.ByteBuffer; import java.util.*; +import java.util.Collections; import static ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED; import static ch.dissem.bitmessage.entity.Plaintext.Encoding.SIMPLE; @@ -50,6 +48,7 @@ public class Plaintext implements Streamable { private final long encoding; private final byte[] message; private final byte[] ackData; + private final UUID conversationId; private ExtendedEncoding extendedData; private ObjectMessage ackMessage; private Object id; @@ -90,6 +89,7 @@ public class Plaintext implements Streamable { ttl = builder.ttl; retries = builder.retries; nextTry = builder.nextTry; + conversationId = builder.conversation; } public static Plaintext read(Type type, InputStream in) throws IOException { @@ -390,7 +390,7 @@ public class Plaintext implements Streamable { } public List<InventoryVector> getParents() { - if (Message.TYPE.equals(getExtendedData().getType())) { + if (getExtendedData() != null && Message.TYPE.equals(getExtendedData().getType())) { return ((Message) extendedData.getContent()).getParents(); } else { return Collections.emptyList(); @@ -405,6 +405,10 @@ public class Plaintext implements Streamable { } } + public UUID getConversationId() { + return conversationId; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -470,6 +474,16 @@ public class Plaintext implements Streamable { return initialHash; } + @Override + public String toString() { + String subject = getSubject(); + if (subject == null || subject.length() == 0) { + return Strings.hex(initialHash).toString(); + } else { + return subject; + } + } + public enum Encoding { IGNORE(0), TRIVIAL(1), SIMPLE(2), EXTENDED(3); @@ -527,12 +541,13 @@ public class Plaintext implements Streamable { private byte[] ackMessage; private byte[] signature; private long sent; - private long received; + private Long received; private Status status; private Set<Label> labels = new HashSet<>(); private long ttl; private int retries; private Long nextTry; + private UUID conversation; public Builder(Type type) { this.type = type; @@ -685,6 +700,11 @@ public class Plaintext implements Streamable { return this; } + public Builder conversation(UUID id) { + this.conversation = id; + return this; + } + public Plaintext build() { if (from == null) { from = new BitmessageAddress(Factory.createPubkey( @@ -706,6 +726,9 @@ public class Plaintext implements Streamable { if (ttl <= 0) { ttl = TTL.msg(); } + if (conversation == null) { + conversation = UUID.randomUUID(); + } return new Plaintext(this); } } diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/AbstractMessageRepository.java b/core/src/main/java/ch/dissem/bitmessage/ports/AbstractMessageRepository.java index 5e51ace..6184ff9 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/AbstractMessageRepository.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/AbstractMessageRepository.java @@ -19,13 +19,16 @@ package ch.dissem.bitmessage.ports; import ch.dissem.bitmessage.InternalContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.utils.Strings; import ch.dissem.bitmessage.utils.UnixTime; import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.UUID; import static ch.dissem.bitmessage.utils.SqlStrings.join; @@ -71,6 +74,11 @@ public abstract class AbstractMessageRepository implements MessageRepository, In } } + @Override + public Plaintext getMessage(InventoryVector iv) { + return single(find("iv=X'" + Strings.hex(iv.getHash()) + "'")); + } + @Override public Plaintext getMessage(byte[] initialHash) { return single(find("initial_hash=X'" + Strings.hex(initialHash) + "'")); @@ -111,6 +119,20 @@ public abstract class AbstractMessageRepository implements MessageRepository, In " AND next_try < " + UnixTime.now()); } + @Override + public List<Plaintext> findResponses(Plaintext parent) { + if (parent.getInventoryVector() == null) { + return Collections.emptyList(); + } + return find("iv IN (SELECT child FROM Message_Parent" + + " WHERE parent=X'" + Strings.hex(parent.getInventoryVector().getHash()) + "')"); + } + + @Override + public List<Plaintext> getConversation(UUID conversationId) { + return find("conversation=X'" + conversationId.toString().replace("-", "") + "'"); + } + @Override public List<Label> getLabels() { return findLabels("1=1"); diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/MessageRepository.java b/core/src/main/java/ch/dissem/bitmessage/ports/MessageRepository.java index aca16b9..ba34cfe 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/MessageRepository.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/MessageRepository.java @@ -19,9 +19,12 @@ package ch.dissem.bitmessage.ports; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.Plaintext.Status; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; +import java.util.Collection; import java.util.List; +import java.util.UUID; public interface MessageRepository { List<Label> getLabels(); @@ -32,10 +35,18 @@ public interface MessageRepository { Plaintext getMessage(Object id); + Plaintext getMessage(InventoryVector iv); + Plaintext getMessage(byte[] initialHash); Plaintext getMessageForAck(byte[] ackData); + /** + * @param label to search for + * @return a distinct list of all conversations that have at least one message with the given label. + */ + List<UUID> findConversations(Label label); + List<Plaintext> findMessages(Label label); List<Plaintext> findMessages(Status status); @@ -44,9 +55,21 @@ public interface MessageRepository { List<Plaintext> findMessages(BitmessageAddress sender); + List<Plaintext> findResponses(Plaintext parent); + List<Plaintext> findMessagesToResend(); void save(Plaintext message); void remove(Plaintext message); + + /** + * Returns all messages with this conversation ID. The returned messages aren't sorted in any way, + * so you may prefer to use {@link ch.dissem.bitmessage.utils.ConversationService#getConversation(UUID)} + * instead. + * + * @param conversationId ID of the requested conversation + * @return all messages with the given conversation ID + */ + Collection<Plaintext> getConversation(UUID conversationId); } diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/NodeRegistry.java b/core/src/main/java/ch/dissem/bitmessage/ports/NodeRegistry.java index a1d33ad..ad5219b 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/NodeRegistry.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/NodeRegistry.java @@ -24,6 +24,12 @@ import java.util.List; * Stores and provides known peers. */ public interface NodeRegistry { + /** + * Removes all known nodes from registry. This should work around connection issues + * when there are many invalid nodes in the registry. + */ + void clear(); + List<NetworkAddress> getKnownAddresses(int limit, long... streams); void offerAddresses(List<NetworkAddress> addresses); diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/ConversationService.java b/core/src/main/java/ch/dissem/bitmessage/utils/ConversationService.java new file mode 100644 index 0000000..a4d8a44 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/utils/ConversationService.java @@ -0,0 +1,112 @@ +/* + * Copyright 2017 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.utils; + +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import ch.dissem.bitmessage.ports.MessageRepository; + +import java.util.*; +import java.util.Collections; + +/** + * Helper service to work with conversations + */ +public class ConversationService { + private final MessageRepository messageRepository; + + public ConversationService(MessageRepository messageRepository) { + this.messageRepository = messageRepository; + } + + public List<Plaintext> getConversation(Plaintext message) { + return getConversation(message.getConversationId()); + } + + private LinkedList<Plaintext> sorted(Collection<Plaintext> collection) { + LinkedList<Plaintext> result = new LinkedList<>(collection); + Collections.sort(result, new Comparator<Plaintext>() { + @Override + public int compare(Plaintext o1, Plaintext o2) { + //noinspection NumberEquality - if both are null (if both are the same, it's a bonus) + if (o1.getReceived() == o2.getReceived()) { + return 0; + } + if (o1.getReceived() == null) { + return -1; + } + if (o2.getReceived() == null) { + return 1; + } + return -o1.getReceived().compareTo(o2.getReceived()); + } + }); + return result; + } + + public List<Plaintext> getConversation(UUID conversationId) { + LinkedList<Plaintext> messages = sorted(messageRepository.getConversation(conversationId)); + Map<InventoryVector, Plaintext> map = new HashMap<>(messages.size()); + for (Plaintext message : messages) { + if (message.getInventoryVector() != null) { + map.put(message.getInventoryVector(), message); + } + } + + LinkedList<Plaintext> result = new LinkedList<>(); + while (!messages.isEmpty()) { + Plaintext last = messages.poll(); + int pos = lastParentPosition(last, result); + result.add(pos, last); + addAncestors(last, result, messages, map); + } + return result; + } + + private int lastParentPosition(Plaintext child, LinkedList<Plaintext> messages) { + Iterator<Plaintext> plaintextIterator = messages.descendingIterator(); + int i = 0; + while (plaintextIterator.hasNext()) { + Plaintext next = plaintextIterator.next(); + if (isParent(next, child)) { + break; + } + i++; + } + return messages.size() - i; + } + + private boolean isParent(Plaintext item, Plaintext child) { + for (InventoryVector parentId : child.getParents()) { + if (parentId.equals(item.getInventoryVector())) { + return true; + } + } + return false; + } + + private void addAncestors(Plaintext message, LinkedList<Plaintext> result, LinkedList<Plaintext> messages, Map<InventoryVector, Plaintext> map) { + for (InventoryVector parentKey : message.getParents()) { + Plaintext parent = map.remove(parentKey); + if (parent != null) { + messages.remove(parent); + result.addFirst(parent); + addAncestors(parent, result, messages, map); + } + } + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Decode.java b/core/src/main/java/ch/dissem/bitmessage/utils/Decode.java index c15f397..8d05e7e 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Decode.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Decode.java @@ -138,7 +138,7 @@ public class Decode { public static String varString(InputStream in, AccessCounter counter) throws IOException { int length = (int) varInt(in, counter); - // FIXME: technically, it says the length in characters, but I think this one might be correct + // technically, it says the length in characters, but I think this one might be correct // otherwise it will get complicated, as we'll need to read UTF-8 char by char... return new String(bytes(in, length, counter), "utf-8"); } diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/ConversationServiceTest.java b/core/src/test/java/ch/dissem/bitmessage/utils/ConversationServiceTest.java new file mode 100644 index 0000000..c95c066 --- /dev/null +++ b/core/src/test/java/ch/dissem/bitmessage/utils/ConversationServiceTest.java @@ -0,0 +1,126 @@ +/* + * Copyright 2017 Christian Basler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ch.dissem.bitmessage.utils; + +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; +import ch.dissem.bitmessage.entity.valueobject.extended.Message; +import ch.dissem.bitmessage.ports.MessageRepository; +import org.junit.Test; + +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; +import static ch.dissem.bitmessage.utils.TestUtils.RANDOM; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ConversationServiceTest { + private BitmessageAddress alice = new BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); + private BitmessageAddress bob = new BitmessageAddress("BM-2cTtkBnb4BUYDndTKun6D9PjtueP2h1bQj"); + + private MessageRepository messageRepository = mock(MessageRepository.class); + private ConversationService conversationService = new ConversationService(messageRepository); + + static { + Singleton.initialize(new BouncyCryptography()); + } + + @Test + public void ensureConversationIsSortedProperly() { + List<Plaintext> expected = getConversation(); + + when(conversationService.getConversation(any(UUID.class))).thenReturn(expected); + List<Plaintext> actual = conversationService.getConversation(UUID.randomUUID()); + assertThat(actual, is(expected)); + } + + private List<Plaintext> getConversation() { + List<Plaintext> result = new LinkedList<>(); + + Plaintext older = plaintext(alice, bob, + new Message.Builder() + .subject("hey there") + .body("does it work?") + .build(), + Plaintext.Status.SENT); + result.add(older); + + Plaintext root = plaintext(alice, bob, + new Message.Builder() + .subject("new test") + .body("There's a new test in town!") + .build(), + Plaintext.Status.SENT); + result.add(root); + + result.add( + plaintext(bob, alice, + new Message.Builder() + .subject("Re: new test (1a)") + .body("Nice!") + .addParent(root) + .build(), + Plaintext.Status.RECEIVED) + ); + + Plaintext latest = plaintext(bob, alice, + new Message.Builder() + .subject("Re: new test (2b)") + .body("PS: it did work!") + .addParent(root) + .addParent(older) + .build(), + Plaintext.Status.RECEIVED); + result.add(latest); + + result.add( + plaintext(alice, bob, + new Message.Builder() + .subject("Re: new test (2)") + .body("") + .addParent(latest) + .build(), + Plaintext.Status.DRAFT) + ); + + return result; + } + + private int timer = 2; + + private Plaintext plaintext(BitmessageAddress from, BitmessageAddress to, + ExtendedEncoding content, Plaintext.Status status) { + Plaintext.Builder builder = new Plaintext.Builder(MSG) + .IV(TestUtils.randomInventoryVector()) + .from(from) + .to(to) + .message(content) + .status(status); + if (status != Plaintext.Status.DRAFT && status != Plaintext.Status.DOING_PROOF_OF_WORK) { + builder.received(5 * ++timer - RANDOM.nextInt(10)); + } + return builder.build(); + } +} diff --git a/demo/src/test/java/ch/dissem/bitmessage/TestNodeRegistry.java b/demo/src/test/java/ch/dissem/bitmessage/TestNodeRegistry.java index 71750ed..2f9f070 100644 --- a/demo/src/test/java/ch/dissem/bitmessage/TestNodeRegistry.java +++ b/demo/src/test/java/ch/dissem/bitmessage/TestNodeRegistry.java @@ -39,6 +39,11 @@ class TestNodeRegistry implements NodeRegistry { } } + @Override + public void clear() { + // NO OP + } + @Override public List<NetworkAddress> getKnownAddresses(int limit, long... streams) { return nodes; diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java index 9851eb4..435e54f 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java @@ -31,6 +31,7 @@ import java.sql.*; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import java.util.UUID; import static ch.dissem.bitmessage.repository.JdbcHelper.writeBlob; @@ -99,7 +100,7 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements Connection connection = config.getConnection(); Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery( - "SELECT id, iv, type, sender, recipient, data, ack_data, sent, received, initial_hash, status, ttl, retries, next_try " + + "SELECT id, iv, type, sender, recipient, data, ack_data, sent, received, initial_hash, status, ttl, retries, next_try, conversation " + "FROM Message WHERE " + where) ) { while (rs.next()) { @@ -119,6 +120,7 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements builder.ttl(rs.getLong("ttl")); builder.retries(rs.getInt("retries")); builder.nextTry(rs.getLong("next_try")); + builder.conversation((UUID) rs.getObject("conversation")); builder.labels(findLabels(connection, "id IN (SELECT label_id FROM Message_Label WHERE message_id=" + id + ") ORDER BY ord")); Plaintext message = builder.build(); @@ -155,6 +157,7 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements try { connection.setAutoCommit(false); save(connection, message); + updateParents(connection, message); updateLabels(connection, message); connection.commit(); } catch (IOException | SQLException e) { @@ -189,11 +192,38 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements } } + private void updateParents(Connection connection, Plaintext message) throws SQLException { + if (message.getInventoryVector() == null || message.getParents().isEmpty()) { + // There are no parents to save yet (they are saved in the extended data, that's enough for now) + return; + } + // remove existing parents + try (PreparedStatement ps = connection.prepareStatement("DELETE FROM Message_Parent WHERE child=?")) { + ps.setBytes(1, message.getInitialHash()); + ps.executeUpdate(); + } + byte[] childIV = message.getInventoryVector().getHash(); + // save new parents + int order = 0; + for (InventoryVector parentIV : message.getParents()) { + Plaintext parent = getMessage(parentIV); + mergeConversations(connection, parent.getConversationId(), message.getConversationId()); + order++; + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO Message_Parent VALUES (?, ?, ?, ?)")) { + ps.setBytes(1, parentIV.getHash()); + ps.setBytes(2, childIV); + ps.setInt(3, order); // FIXME: this might not be necessary + ps.setObject(4, message.getConversationId()); + ps.executeUpdate(); + } + } + } + private void insert(Connection connection, Plaintext message) throws SQLException, IOException { try (PreparedStatement ps = connection.prepareStatement( "INSERT INTO Message (iv, type, sender, recipient, data, ack_data, sent, received, " + - "status, initial_hash, ttl, retries, next_try) " + - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "status, initial_hash, ttl, retries, next_try, conversation) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS) ) { ps.setBytes(1, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash()); @@ -209,6 +239,7 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements ps.setLong(11, message.getTTL()); ps.setInt(12, message.getRetries()); ps.setObject(13, message.getNextTry()); + ps.setObject(14, message.getConversationId()); ps.executeUpdate(); // get generated id @@ -262,4 +293,52 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements LOG.error(e.getMessage(), e); } } + + @Override + public List<UUID> findConversations(Label label) { + String where; + if (label == null) { + where = "id NOT IN (SELECT message_id FROM Message_Label)"; + } else { + where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")"; + } + List<UUID> result = new LinkedList<>(); + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT DISTINCT conversation FROM Message WHERE " + where) + ) { + while (rs.next()) { + result.add((UUID) rs.getObject(1)); + } + } catch (SQLException e) { + LOG.error(e.getMessage(), e); + } + return result; + } + + /** + * Replaces every occurrence of the source conversation ID with the target ID + * + * @param source ID of the conversation to be merged + * @param target ID of the merge target + */ + private void mergeConversations(Connection connection, UUID source, UUID target) { + try ( + PreparedStatement ps1 = connection.prepareStatement( + "UPDATE Message SET conversation=? WHERE conversation=?"); + PreparedStatement ps2 = connection.prepareStatement( + "UPDATE Message_Parent SET conversation=? WHERE conversation=?") + ) { + ps1.setObject(1, target); + ps1.setObject(2, source); + ps1.executeUpdate(); + ps2.setObject(1, target); + ps2.setObject(2, source); + ps2.executeUpdate(); + } catch (SQLException e) { + LOG.error(e.getMessage(), e); + } + } } diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcNodeRegistry.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcNodeRegistry.java index 8dd006f..a889db6 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcNodeRegistry.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcNodeRegistry.java @@ -69,6 +69,19 @@ public class JdbcNodeRegistry extends JdbcHelper implements NodeRegistry { } } + @Override + public void clear() { + try ( + Connection connection = config.getConnection(); + PreparedStatement ps = connection.prepareStatement( + "DELETE FROM Node") + ) { + ps.executeUpdate(); + } catch (SQLException e) { + LOG.error(e.getMessage(), e); + } + } + @Override public List<NetworkAddress> getKnownAddresses(int limit, long... streams) { List<NetworkAddress> result = new LinkedList<>(); diff --git a/repositories/src/main/resources/db/migration/V4.0__Create_table_message_parent.sql b/repositories/src/main/resources/db/migration/V4.0__Create_table_message_parent.sql new file mode 100644 index 0000000..8b5f0ca --- /dev/null +++ b/repositories/src/main/resources/db/migration/V4.0__Create_table_message_parent.sql @@ -0,0 +1,11 @@ +ALTER TABLE Message ADD COLUMN conversation UUID NOT NULL DEFAULT RANDOM_UUID(); + +CREATE TABLE Message_Parent ( + parent BINARY(64) NOT NULL, + child BINARY(64) NOT NULL, + pos INT NOT NULL, + conversation UUID, + + PRIMARY KEY (parent, child), + FOREIGN KEY (child) REFERENCES Message (iv) +); diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java index 0d3feec..d735093 100644 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java @@ -21,18 +21,23 @@ import ch.dissem.bitmessage.InternalContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; +import ch.dissem.bitmessage.entity.valueobject.extended.Message; import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.MessageRepository; import ch.dissem.bitmessage.utils.TestUtils; import ch.dissem.bitmessage.utils.UnixTime; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; import org.junit.Before; import org.junit.Test; import java.util.Arrays; import java.util.List; +import java.util.UUID; import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK; @@ -49,6 +54,7 @@ public class JdbcMessageRepositoryTest extends TestBase { private MessageRepository repo; private Label inbox; + private Label sent; private Label drafts; private Label unread; @@ -59,9 +65,9 @@ public class JdbcMessageRepositoryTest extends TestBase { AddressRepository addressRepo = new JdbcAddressRepository(config); repo = new JdbcMessageRepository(config); new InternalContext(new BitmessageContext.Builder() - .cryptography(cryptography()) - .addressRepo(addressRepo) - .messageRepo(repo) + .cryptography(cryptography()) + .addressRepo(addressRepo) + .messageRepo(repo) ); BitmessageAddress tmp = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000, DOES_ACK)); @@ -75,6 +81,7 @@ public class JdbcMessageRepositoryTest extends TestBase { addressRepo.save(identity); inbox = repo.getLabels(Label.Type.INBOX).get(0); + sent = repo.getLabels(Label.Type.SENT).get(0); drafts = repo.getLabels(Label.Type.DRAFT).get(0); unread = repo.getLabels(Label.Type.UNREAD).get(0); @@ -163,12 +170,12 @@ public class JdbcMessageRepositoryTest extends TestBase { @Test public void testSave() throws Exception { Plaintext message = new Plaintext.Builder(MSG) - .IV(TestUtils.randomInventoryVector()) - .from(identity) - .to(contactA) - .message("Subject", "Message") - .status(Plaintext.Status.DOING_PROOF_OF_WORK) - .build(); + .IV(TestUtils.randomInventoryVector()) + .from(identity) + .to(contactA) + .message("Subject", "Message") + .status(Plaintext.Status.DOING_PROOF_OF_WORK) + .build(); repo.save(message); assertNotNull(message.getId()); @@ -207,19 +214,19 @@ public class JdbcMessageRepositoryTest extends TestBase { @Test public void ensureUnacknowledgedMessagesAreFoundForResend() throws Exception { Plaintext message = new Plaintext.Builder(MSG) - .IV(TestUtils.randomInventoryVector()) - .from(identity) - .to(contactA) - .message("Subject", "Message") - .status(Plaintext.Status.SENT) - .ttl(2) - .build(); + .IV(TestUtils.randomInventoryVector()) + .from(identity) + .to(contactA) + .message("Subject", "Message") + .status(Plaintext.Status.SENT) + .ttl(2) + .build(); message.updateNextTry(); assertThat(message.getRetries(), is(1)); assertThat(message.getNextTry(), greaterThan(UnixTime.now())); assertThat(message.getNextTry(), lessThanOrEqualTo(UnixTime.now(+2))); repo.save(message); - Thread.sleep(4100); + Thread.sleep(4100); // somewhat longer than 2*TTL List<Plaintext> messagesToResend = repo.findMessagesToResend(); assertThat(messagesToResend, hasSize(1)); @@ -231,14 +238,105 @@ public class JdbcMessageRepositoryTest extends TestBase { assertThat(messagesToResend, empty()); } - private void addMessage(BitmessageAddress from, BitmessageAddress to, Plaintext.Status status, Label... labels) { + @Test + public void ensureParentsAreSaved() { + Plaintext parent = storeConversation(); + + List<Plaintext> responses = repo.findResponses(parent); + assertThat(responses, hasSize(2)); + assertThat(responses, hasItem(hasMessage("Re: new test", "Nice!"))); + assertThat(responses, hasItem(hasMessage("Re: new test", "PS: it did work!"))); + } + + @Test + public void ensureConversationCanBeRetrieved() { + Plaintext root = storeConversation(); + List<UUID> conversations = repo.findConversations(inbox); + assertThat(conversations, hasSize(2)); + assertThat(conversations, hasItem(root.getConversationId())); + } + + private Plaintext addMessage(BitmessageAddress from, BitmessageAddress to, Plaintext.Status status, Label... labels) { + ExtendedEncoding content = new Message.Builder() + .subject("Subject") + .body("Message") + .build(); + return addMessage(from, to, content, status, labels); + } + + private Plaintext addMessage(BitmessageAddress from, BitmessageAddress to, + ExtendedEncoding content, Plaintext.Status status, Label... labels) { Plaintext message = new Plaintext.Builder(MSG) - .from(from) - .to(to) - .message("Subject", "Message") - .status(status) - .labels(Arrays.asList(labels)) - .build(); + .IV(TestUtils.randomInventoryVector()) + .from(from) + .to(to) + .message(content) + .status(status) + .labels(Arrays.asList(labels)) + .build(); repo.save(message); + return message; + } + + private Plaintext storeConversation() { + Plaintext older = addMessage(identity, contactA, + new Message.Builder() + .subject("hey there") + .body("does it work?") + .build(), + Plaintext.Status.SENT, sent); + + Plaintext root = addMessage(identity, contactA, + new Message.Builder() + .subject("new test") + .body("There's a new test in town!") + .build(), + Plaintext.Status.SENT, sent); + + addMessage(contactA, identity, + new Message.Builder() + .subject("Re: new test") + .body("Nice!") + .addParent(root) + .build(), + Plaintext.Status.RECEIVED, inbox); + + addMessage(contactA, identity, + new Message.Builder() + .subject("Re: new test") + .body("PS: it did work!") + .addParent(root) + .addParent(older) + .build(), + Plaintext.Status.RECEIVED, inbox); + + return repo.getMessage(root.getId()); + } + + private Matcher<Plaintext> hasMessage(String subject, String body) { + return new BaseMatcher<Plaintext>() { + @Override + public void describeTo(Description description) { + description.appendText("Subject: ").appendText(subject); + description.appendText(", "); + description.appendText("Body: ").appendText(body); + } + + @Override + public boolean matches(Object item) { + if (item instanceof Plaintext) { + Plaintext message = (Plaintext) item; + if (subject != null && !subject.equals(message.getSubject())) { + return false; + } + if (body != null && !body.equals(message.getText())) { + return false; + } + return true; + } else { + return false; + } + } + }; } } diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/TestJdbcConfig.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/TestJdbcConfig.java index 99a26c0..edb8f85 100644 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/TestJdbcConfig.java +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/TestJdbcConfig.java @@ -16,11 +16,27 @@ package ch.dissem.bitmessage.repository; +import org.h2.tools.Server; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.SQLException; + /** * JdbcConfig to be used for tests. Uses an in-memory database and adds a useful {@link #reset()} method resetting * the database. */ public class TestJdbcConfig extends JdbcConfig { + private static final Logger LOG = LoggerFactory.getLogger(TestJdbcConfig.class); + + static { + try { + Server.createTcpServer().start(); + } catch (SQLException e) { + LOG.error(e.getMessage(), e); + } + } + public TestJdbcConfig() { super("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", "sa", null); } From 3ab3d7a0ca82355b09c76b603293b636fdd85a2d Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 30 Mar 2017 16:30:46 +0200 Subject: [PATCH 12/22] ConversationService additions, fixed migration script --- .../bitmessage/utils/ConversationService.java | 32 ++++++++++++++++++- .../V4.0__Create_table_message_parent.sql | 4 +-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/ConversationService.java b/core/src/main/java/ch/dissem/bitmessage/utils/ConversationService.java index a4d8a44..f261a08 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/ConversationService.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/ConversationService.java @@ -22,18 +22,35 @@ import ch.dissem.bitmessage.ports.MessageRepository; import java.util.*; import java.util.Collections; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.regex.Pattern.CASE_INSENSITIVE; /** - * Helper service to work with conversations + * Service that helps with conversations. */ public class ConversationService { private final MessageRepository messageRepository; + private final Pattern SUBJECT_PREFIX = Pattern.compile("^(re|fwd?):", CASE_INSENSITIVE); + public ConversationService(MessageRepository messageRepository) { this.messageRepository = messageRepository; } + /** + * Retrieve the whole conversation from one single message. If the message isn't part + * of a conversation, a singleton list containing the given message is returned. Otherwise + * it's the same as {@link #getConversation(UUID)} + * + * @param message + * @return a list of messages that belong to the same conversation. + */ public List<Plaintext> getConversation(Plaintext message) { + if (message.getConversationId() == null) { + return Collections.singletonList(message); + } return getConversation(message.getConversationId()); } @@ -77,6 +94,19 @@ public class ConversationService { return result; } + public String getSubject(List<Plaintext> conversation) { + if (conversation.isEmpty()) { + return null; + } + // TODO: this has room for improvement + String subject = conversation.get(0).getSubject(); + Matcher matcher = SUBJECT_PREFIX.matcher(subject); + if (matcher.find()) { + return subject.substring(matcher.end()).trim(); + } + return subject.trim(); + } + private int lastParentPosition(Plaintext child, LinkedList<Plaintext> messages) { Iterator<Plaintext> plaintextIterator = messages.descendingIterator(); int i = 0; diff --git a/repositories/src/main/resources/db/migration/V4.0__Create_table_message_parent.sql b/repositories/src/main/resources/db/migration/V4.0__Create_table_message_parent.sql index 8b5f0ca..aa0a120 100644 --- a/repositories/src/main/resources/db/migration/V4.0__Create_table_message_parent.sql +++ b/repositories/src/main/resources/db/migration/V4.0__Create_table_message_parent.sql @@ -1,10 +1,10 @@ -ALTER TABLE Message ADD COLUMN conversation UUID NOT NULL DEFAULT RANDOM_UUID(); +ALTER TABLE Message ADD COLUMN conversation UUID; CREATE TABLE Message_Parent ( parent BINARY(64) NOT NULL, child BINARY(64) NOT NULL, pos INT NOT NULL, - conversation UUID, + conversation UUID NOT NULL, PRIMARY KEY (parent, child), FOREIGN KEY (child) REFERENCES Message (iv) From 5849e68d2069d139840bf71a1b15ca9d76cd59fe Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 30 Mar 2017 16:31:36 +0200 Subject: [PATCH 13/22] Keep order of labels --- .../java/ch/dissem/bitmessage/entity/Plaintext.java | 2 +- .../bitmessage/repository/JdbcMessageRepository.java | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java index dad561d..9d09433 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java @@ -543,7 +543,7 @@ public class Plaintext implements Streamable { private long sent; private Long received; private Status status; - private Set<Label> labels = new HashSet<>(); + private Set<Label> labels = new LinkedHashSet<>(); private long ttl; private int retries; private Long nextTry; diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java index 435e54f..442a6bb 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java @@ -205,11 +205,11 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements byte[] childIV = message.getInventoryVector().getHash(); // save new parents int order = 0; - for (InventoryVector parentIV : message.getParents()) { - Plaintext parent = getMessage(parentIV); - mergeConversations(connection, parent.getConversationId(), message.getConversationId()); - order++; - try (PreparedStatement ps = connection.prepareStatement("INSERT INTO Message_Parent VALUES (?, ?, ?, ?)")) { + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO Message_Parent VALUES (?, ?, ?, ?)")) { + for (InventoryVector parentIV : message.getParents()) { + Plaintext parent = getMessage(parentIV); + mergeConversations(connection, parent.getConversationId(), message.getConversationId()); + order++; ps.setBytes(1, parentIV.getHash()); ps.setBytes(2, childIV); ps.setInt(3, order); // FIXME: this might not be necessary From 016b4f80baba5b6c11c6918c6aa24a302f36688e Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 31 Mar 2017 17:07:57 +0200 Subject: [PATCH 14/22] Code fix --- demo/src/main/java/ch/dissem/bitmessage/demo/Main.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/demo/src/main/java/ch/dissem/bitmessage/demo/Main.java b/demo/src/main/java/ch/dissem/bitmessage/demo/Main.java index c2cc7a5..1da2910 100644 --- a/demo/src/main/java/ch/dissem/bitmessage/demo/Main.java +++ b/demo/src/main/java/ch/dissem/bitmessage/demo/Main.java @@ -65,6 +65,11 @@ public class Main { .port(48444); if (options.localPort != null) { ctxBuilder.nodeRegistry(new NodeRegistry() { + @Override + public void clear() { + // NO OP + } + @Override public List<NetworkAddress> getKnownAddresses(int limit, long... streams) { return Arrays.stream(streams) From 841fb7eccd95bc73a872711df48136114a12698f Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 2 Apr 2017 21:02:20 +0200 Subject: [PATCH 15/22] Test fixes and improvements --- .../dissem/bitmessage/entity/Plaintext.java | 10 ++-- .../bitmessage/BitmessageContextTest.java | 52 ++++++++++++------- .../bitmessage/entity/SerializationTest.java | 41 ++++++++------- .../bitmessage/testutils}/TestInventory.java | 2 +- .../utils/ConversationServiceTest.java | 2 +- cryptography-bc/build.gradle | 2 +- .../networking/NetworkHandlerTest.java | 1 + .../networking/TestNodeRegistry.java | 5 ++ repositories/build.gradle | 2 +- .../repository/JdbcMessageRepository.java | 16 +++--- .../repository/JdbcMessageRepositoryTest.java | 1 + 11 files changed, 78 insertions(+), 56 deletions(-) rename {networking/src/test/java/ch/dissem/bitmessage/networking => core/src/test/java/ch/dissem/bitmessage/testutils}/TestInventory.java (98%) diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java index 9d09433..141edfc 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java @@ -439,9 +439,7 @@ public class Plaintext implements Streamable { public void addLabels(Collection<Label> labels) { if (labels != null) { - for (Label label : labels) { - this.labels.add(label); - } + this.labels.addAll(labels); } } @@ -540,7 +538,7 @@ public class Plaintext implements Streamable { private byte[] ackData; private byte[] ackMessage; private byte[] signature; - private long sent; + private Long sent; private Long received; private Status status; private Set<Label> labels = new LinkedHashSet<>(); @@ -665,12 +663,12 @@ public class Plaintext implements Streamable { return this; } - public Builder sent(long sent) { + public Builder sent(Long sent) { this.sent = sent; return this; } - public Builder received(long received) { + public Builder received(Long received) { this.received = received; return this; } diff --git a/core/src/test/java/ch/dissem/bitmessage/BitmessageContextTest.java b/core/src/test/java/ch/dissem/bitmessage/BitmessageContextTest.java index b4f536b..8051134 100644 --- a/core/src/test/java/ch/dissem/bitmessage/BitmessageContextTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/BitmessageContextTest.java @@ -25,12 +25,11 @@ import ch.dissem.bitmessage.entity.payload.ObjectType; import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.ports.*; +import ch.dissem.bitmessage.testutils.TestInventory; import ch.dissem.bitmessage.utils.MessageMatchers; import ch.dissem.bitmessage.utils.Singleton; import ch.dissem.bitmessage.utils.TTL; import ch.dissem.bitmessage.utils.TestUtils; -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; import org.junit.Before; import org.junit.Test; @@ -51,15 +50,18 @@ import static org.mockito.Mockito.*; public class BitmessageContextTest { private BitmessageContext ctx; private BitmessageContext.Listener listener; + private TestInventory testInventory; @Before public void setUp() throws Exception { Singleton.initialize(null); listener = mock(BitmessageContext.Listener.class); + Singleton.initialize(new BouncyCryptography()); + testInventory = new TestInventory(); ctx = new BitmessageContext.Builder() .addressRepo(mock(AddressRepository.class)) - .cryptography(new BouncyCryptography()) - .inventory(mock(Inventory.class)) + .cryptography(cryptography()) + .inventory(spy(testInventory)) .listener(listener) .messageRepo(mock(MessageRepository.class)) .networkHandler(mock(NetworkHandler.class)) @@ -134,11 +136,19 @@ public class BitmessageContextTest { @Test public void ensureV2PubkeyIsNotRequestedIfItExistsInInventory() throws Exception { + testInventory.init( + "V1Msg.payload", + "V2GetPubkey.payload", + "V2Pubkey.payload", + "V3GetPubkey.payload", + "V3Pubkey.payload", + "V4Broadcast.payload", + "V4GetPubkey.payload", + "V4Pubkey.payload", + "V5Broadcast.payload" + ); BitmessageAddress contact = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"); - when(ctx.internals().getInventory().getObjects(anyLong(), anyLong(), any(ObjectType.class))) - .thenReturn(Collections.singletonList( - TestUtils.loadObjectMessage(2, "V2Pubkey.payload") - )); + when(ctx.addresses().getAddress(contact.getAddress())).thenReturn(contact); ctx.addContact(contact); @@ -150,11 +160,18 @@ public class BitmessageContextTest { @Test public void ensureV4PubkeyIsNotRequestedIfItExistsInInventory() throws Exception { + testInventory.init( + "V1Msg.payload", + "V2GetPubkey.payload", + "V2Pubkey.payload", + "V3GetPubkey.payload", + "V3Pubkey.payload", + "V4Broadcast.payload", + "V4GetPubkey.payload", + "V4Pubkey.payload", + "V5Broadcast.payload" + ); BitmessageAddress contact = new BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h"); - when(ctx.internals().getInventory().getObjects(anyLong(), anyLong(), any(ObjectType.class))) - .thenReturn(Collections.singletonList( - TestUtils.loadObjectMessage(2, "V4Pubkey.payload") - )); final BitmessageAddress stored = new BitmessageAddress(contact.getAddress()); stored.setAlias("Test"); when(ctx.addresses().getAddress(contact.getAddress())).thenReturn(stored); @@ -170,13 +187,12 @@ public class BitmessageContextTest { public void ensureSubscriptionIsAddedAndExistingBroadcastsRetrieved() throws Exception { BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ"); - List<ObjectMessage> objects = new LinkedList<>(); - objects.add(TestUtils.loadObjectMessage(4, "V4Broadcast.payload")); - objects.add(TestUtils.loadObjectMessage(5, "V5Broadcast.payload")); - when(ctx.internals().getInventory().getObjects(eq(address.getStream()), anyLong(), any(ObjectType.class))) - .thenReturn(objects); - when(ctx.addresses().getSubscriptions(anyLong())).thenReturn(Collections.singletonList(address)); + testInventory.init( + "V4Broadcast.payload", + "V5Broadcast.payload" + ); + when(ctx.addresses().getSubscriptions(anyLong())).thenReturn(Collections.singletonList(address)); ctx.addSubscribtion(address); verify(ctx.addresses(), atLeastOnce()).save(address); diff --git a/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java b/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java index aacc00f..3e96b77 100644 --- a/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java @@ -31,6 +31,7 @@ import java.util.ArrayList; import java.util.Collections; import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.*; public class SerializationTest extends TestBase { @@ -78,7 +79,7 @@ public class SerializationTest extends TestBase { @Test public void ensurePlaintextIsSerializedAndDeserializedCorrectly() throws Exception { - Plaintext p1 = new Plaintext.Builder(MSG) + Plaintext expected = new Plaintext.Builder(MSG) .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) .to(TestUtils.loadContact()) .message("Subject", "Message") @@ -86,21 +87,21 @@ public class SerializationTest extends TestBase { .signature(new byte[0]) .build(); ByteArrayOutputStream out = new ByteArrayOutputStream(); - p1.write(out); + expected.write(out); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); - Plaintext p2 = Plaintext.read(MSG, in); + Plaintext actual = Plaintext.read(MSG, in); - // Received is automatically set on deserialization, so we'll need to set it to 0 + // Received is automatically set on deserialization, so we'll need to set it to null Field received = Plaintext.class.getDeclaredField("received"); received.setAccessible(true); - received.set(p2, 0L); + received.set(actual, null); - assertEquals(p1, p2); + assertThat(expected, is(actual)); } @Test public void ensurePlaintextWithExtendedEncodingIsSerializedAndDeserializedCorrectly() throws Exception { - Plaintext p1 = new Plaintext.Builder(MSG) + Plaintext expected = new Plaintext.Builder(MSG) .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) .to(TestUtils.loadContact()) .message(new Message.Builder() @@ -111,42 +112,42 @@ public class SerializationTest extends TestBase { .signature(new byte[0]) .build(); ByteArrayOutputStream out = new ByteArrayOutputStream(); - p1.write(out); + expected.write(out); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); - Plaintext p2 = Plaintext.read(MSG, in); + Plaintext actual = Plaintext.read(MSG, in); - // Received is automatically set on deserialization, so we'll need to set it to 0 + // Received is automatically set on deserialization, so we'll need to set it to null Field received = Plaintext.class.getDeclaredField("received"); received.setAccessible(true); - received.set(p2, 0L); + received.set(actual, null); - assertEquals(p1, p2); + assertEquals(expected, actual); } @Test public void ensurePlaintextWithAckMessageIsSerializedAndDeserializedCorrectly() throws Exception { - Plaintext p1 = new Plaintext.Builder(MSG) + Plaintext expected = new Plaintext.Builder(MSG) .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) .to(TestUtils.loadContact()) .message("Subject", "Message") .ackData("ackMessage".getBytes()) .signature(new byte[0]) .build(); - ObjectMessage ackMessage1 = p1.getAckMessage(); + ObjectMessage ackMessage1 = expected.getAckMessage(); assertNotNull(ackMessage1); ByteArrayOutputStream out = new ByteArrayOutputStream(); - p1.write(out); + expected.write(out); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); - Plaintext p2 = Plaintext.read(MSG, in); + Plaintext actual = Plaintext.read(MSG, in); - // Received is automatically set on deserialization, so we'll need to set it to 0 + // Received is automatically set on deserialization, so we'll need to set it to null Field received = Plaintext.class.getDeclaredField("received"); received.setAccessible(true); - received.set(p2, 0L); + received.set(actual, null); - assertEquals(p1, p2); - assertEquals(ackMessage1, p2.getAckMessage()); + assertEquals(expected, actual); + assertEquals(ackMessage1, actual.getAckMessage()); } @Test diff --git a/networking/src/test/java/ch/dissem/bitmessage/networking/TestInventory.java b/core/src/test/java/ch/dissem/bitmessage/testutils/TestInventory.java similarity index 98% rename from networking/src/test/java/ch/dissem/bitmessage/networking/TestInventory.java rename to core/src/test/java/ch/dissem/bitmessage/testutils/TestInventory.java index cefe316..1f8cfd3 100644 --- a/networking/src/test/java/ch/dissem/bitmessage/networking/TestInventory.java +++ b/core/src/test/java/ch/dissem/bitmessage/testutils/TestInventory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package ch.dissem.bitmessage.networking; +package ch.dissem.bitmessage.testutils; import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.payload.ObjectType; diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/ConversationServiceTest.java b/core/src/test/java/ch/dissem/bitmessage/utils/ConversationServiceTest.java index c95c066..d34a122 100644 --- a/core/src/test/java/ch/dissem/bitmessage/utils/ConversationServiceTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/utils/ConversationServiceTest.java @@ -119,7 +119,7 @@ public class ConversationServiceTest { .message(content) .status(status); if (status != Plaintext.Status.DRAFT && status != Plaintext.Status.DOING_PROOF_OF_WORK) { - builder.received(5 * ++timer - RANDOM.nextInt(10)); + builder.received(5L * ++timer - RANDOM.nextInt(10)); } return builder.build(); } diff --git a/cryptography-bc/build.gradle b/cryptography-bc/build.gradle index 0b87c17..b2ffaae 100644 --- a/cryptography-bc/build.gradle +++ b/cryptography-bc/build.gradle @@ -12,7 +12,7 @@ uploadArchives { dependencies { compile project(':core') - compile 'org.bouncycastle:bcprov-jdk15on:1.52' + compile 'org.bouncycastle:bcprov-jdk15on:1.56' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' } diff --git a/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkHandlerTest.java b/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkHandlerTest.java index 940f632..f079ede 100644 --- a/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkHandlerTest.java +++ b/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkHandlerTest.java @@ -24,6 +24,7 @@ import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; import ch.dissem.bitmessage.exception.NodeException; import ch.dissem.bitmessage.networking.nio.NioNetworkHandler; import ch.dissem.bitmessage.ports.*; +import ch.dissem.bitmessage.testutils.TestInventory; import ch.dissem.bitmessage.utils.Property; import org.junit.After; import org.junit.Before; diff --git a/networking/src/test/java/ch/dissem/bitmessage/networking/TestNodeRegistry.java b/networking/src/test/java/ch/dissem/bitmessage/networking/TestNodeRegistry.java index c3abd58..fb00b26 100644 --- a/networking/src/test/java/ch/dissem/bitmessage/networking/TestNodeRegistry.java +++ b/networking/src/test/java/ch/dissem/bitmessage/networking/TestNodeRegistry.java @@ -32,6 +32,11 @@ class TestNodeRegistry implements NodeRegistry { this.nodes = Arrays.asList(nodes); } + @Override + public void clear() { + // no op + } + @Override public List<NetworkAddress> getKnownAddresses(int limit, long... streams) { return nodes; diff --git a/repositories/build.gradle b/repositories/build.gradle index 3f5874b..664279a 100644 --- a/repositories/build.gradle +++ b/repositories/build.gradle @@ -16,7 +16,7 @@ dependencies { compile project(':core') compile 'org.flywaydb:flyway-core:4.0.3' testCompile 'junit:junit:4.12' - testCompile 'com.h2database:h2:1.4.192' + testCompile 'com.h2database:h2:1.4.194' testCompile 'org.mockito:mockito-core:1.10.19' testCompile project(path: ':core', configuration: 'testArtifacts') testCompile project(':cryptography-bc') diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java index 442a6bb..a918456 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java @@ -114,13 +114,13 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements builder.from(ctx.getAddressRepository().getAddress(rs.getString("sender"))); builder.to(ctx.getAddressRepository().getAddress(rs.getString("recipient"))); builder.ackData(rs.getBytes("ack_data")); - builder.sent(rs.getLong("sent")); - builder.received(rs.getLong("received")); + builder.sent(rs.getObject("sent", Long.class)); + builder.received(rs.getObject("received", Long.class)); builder.status(Plaintext.Status.valueOf(rs.getString("status"))); builder.ttl(rs.getLong("ttl")); builder.retries(rs.getInt("retries")); - builder.nextTry(rs.getLong("next_try")); - builder.conversation((UUID) rs.getObject("conversation")); + builder.nextTry(rs.getObject("next_try", Long.class)); + builder.conversation(rs.getObject("conversation", UUID.class)); builder.labels(findLabels(connection, "id IN (SELECT label_id FROM Message_Label WHERE message_id=" + id + ") ORDER BY ord")); Plaintext message = builder.build(); @@ -232,8 +232,8 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements ps.setString(4, message.getTo() == null ? null : message.getTo().getAddress()); writeBlob(ps, 5, message); ps.setBytes(6, message.getAckData()); - ps.setLong(7, message.getSent()); - ps.setLong(8, message.getReceived()); + ps.setObject(7, message.getSent()); + ps.setObject(8, message.getReceived()); ps.setString(9, message.getStatus() == null ? null : message.getStatus().name()); ps.setBytes(10, message.getInitialHash()); ps.setLong(11, message.getTTL()); @@ -261,8 +261,8 @@ public class JdbcMessageRepository extends AbstractMessageRepository implements ps.setString(4, message.getTo() == null ? null : message.getTo().getAddress()); writeBlob(ps, 5, message); ps.setBytes(6, message.getAckData()); - ps.setLong(7, message.getSent()); - ps.setLong(8, message.getReceived()); + ps.setObject(7, message.getSent()); + ps.setObject(8, message.getReceived()); ps.setString(9, message.getStatus() == null ? null : message.getStatus().name()); ps.setBytes(10, message.getInitialHash()); ps.setLong(11, message.getTTL()); diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java index d735093..43be79f 100644 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java @@ -218,6 +218,7 @@ public class JdbcMessageRepositoryTest extends TestBase { .from(identity) .to(contactA) .message("Subject", "Message") + .sent(UnixTime.now()) .status(Plaintext.Status.SENT) .ttl(2) .build(); From c4385b2336dae3be7bec9120dd45424315f015f5 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 2 Apr 2017 21:03:04 +0200 Subject: [PATCH 16/22] Add some passive support for SHA256 based signatures --- .../ports/AbstractCryptography.java | 30 ++++++++++++++++--- .../cryptography/bc/BouncyCryptography.java | 16 ++-------- .../cryptography/sc/SpongyCryptography.java | 12 ++------ 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/AbstractCryptography.java b/core/src/main/java/ch/dissem/bitmessage/ports/AbstractCryptography.java index 7668391..304c20d 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/AbstractCryptography.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/AbstractCryptography.java @@ -31,10 +31,7 @@ import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.io.IOException; import java.math.BigInteger; -import java.security.GeneralSecurityException; -import java.security.MessageDigest; -import java.security.Provider; -import java.security.SecureRandom; +import java.security.*; import static ch.dissem.bitmessage.InternalContext.NETWORK_EXTRA_BYTES; import static ch.dissem.bitmessage.InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE; @@ -50,6 +47,10 @@ public abstract class AbstractCryptography implements Cryptography, InternalCont private static final BigInteger TWO_POW_64 = TWO.pow(64); private static final BigInteger TWO_POW_16 = TWO.pow(16); + protected static final String ALGORITHM_ECDSA = "ECDSA"; + protected static final String ALGORITHM_ECDSA_SHA1 = "SHA1withECDSA"; + protected static final String ALGORITHM_EVP_SHA256 = "SHA256withECDSA"; + protected final Provider provider; private InternalContext context; @@ -127,6 +128,27 @@ public abstract class AbstractCryptography implements Cryptography, InternalCont } } + protected byte[] doSign(byte[] data, java.security.PrivateKey privKey) throws GeneralSecurityException { + // TODO: change this to ALGORITHM_EVP_SHA256 once it's generally used in the network + Signature sig = Signature.getInstance(ALGORITHM_ECDSA_SHA1, provider); + sig.initSign(privKey); + sig.update(data); + return sig.sign(); + } + + + protected boolean doCheckSignature(byte[] data, byte[] signature, PublicKey publicKey) throws GeneralSecurityException { + for (String algorithm : new String[]{ALGORITHM_ECDSA_SHA1, ALGORITHM_EVP_SHA256}) { + Signature sig = Signature.getInstance(algorithm, provider); + sig.initVerify(publicKey); + sig.update(data); + if (sig.verify(signature)) { + return true; + } + } + return false; + } + @Override public byte[] getInitialHash(ObjectMessage object) { return sha512(object.getPayloadBytesWithoutNonce()); diff --git a/cryptography-bc/src/main/java/ch/dissem/bitmessage/cryptography/bc/BouncyCryptography.java b/cryptography-bc/src/main/java/ch/dissem/bitmessage/cryptography/bc/BouncyCryptography.java index 632c30a..bc8ec93 100644 --- a/cryptography-bc/src/main/java/ch/dissem/bitmessage/cryptography/bc/BouncyCryptography.java +++ b/cryptography-bc/src/main/java/ch/dissem/bitmessage/cryptography/bc/BouncyCryptography.java @@ -38,10 +38,7 @@ import org.bouncycastle.jce.spec.ECPublicKeySpec; import org.bouncycastle.math.ec.ECPoint; import java.math.BigInteger; -import java.security.GeneralSecurityException; -import java.security.KeyFactory; -import java.security.PublicKey; -import java.security.Signature; +import java.security.*; import java.security.spec.KeySpec; import java.util.Arrays; @@ -51,7 +48,6 @@ import java.util.Arrays; */ public class BouncyCryptography extends AbstractCryptography { private static final X9ECParameters EC_CURVE_PARAMETERS = CustomNamedCurves.getByName("secp256k1"); - private static final String ALGORITHM_ECDSA = "ECDSA"; public BouncyCryptography() { super(new BouncyCastleProvider()); @@ -106,10 +102,7 @@ public class BouncyCryptography extends AbstractCryptography { KeySpec keySpec = new ECPublicKeySpec(Q, spec); PublicKey publicKey = KeyFactory.getInstance(ALGORITHM_ECDSA, provider).generatePublic(keySpec); - Signature sig = Signature.getInstance(ALGORITHM_ECDSA, provider); - sig.initVerify(publicKey); - sig.update(data); - return sig.verify(signature); + return doCheckSignature(data, signature, publicKey); } catch (GeneralSecurityException e) { throw new ApplicationException(e); } @@ -131,10 +124,7 @@ public class BouncyCryptography extends AbstractCryptography { java.security.PrivateKey privKey = KeyFactory.getInstance(ALGORITHM_ECDSA, provider) .generatePrivate(keySpec); - Signature sig = Signature.getInstance(ALGORITHM_ECDSA, provider); - sig.initSign(privKey); - sig.update(data); - return sig.sign(); + return doSign(data, privKey); } catch (GeneralSecurityException e) { throw new ApplicationException(e); } diff --git a/cryptography-sc/src/main/java/ch/dissem/bitmessage/cryptography/sc/SpongyCryptography.java b/cryptography-sc/src/main/java/ch/dissem/bitmessage/cryptography/sc/SpongyCryptography.java index b9234cd..f86e6c2 100644 --- a/cryptography-sc/src/main/java/ch/dissem/bitmessage/cryptography/sc/SpongyCryptography.java +++ b/cryptography-sc/src/main/java/ch/dissem/bitmessage/cryptography/sc/SpongyCryptography.java @@ -41,7 +41,6 @@ import java.math.BigInteger; import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.PublicKey; -import java.security.Signature; import java.security.spec.KeySpec; import java.util.Arrays; @@ -51,7 +50,6 @@ import java.util.Arrays; */ public class SpongyCryptography extends AbstractCryptography { private static final X9ECParameters EC_CURVE_PARAMETERS = CustomNamedCurves.getByName("secp256k1"); - private static final String ALGORITHM_ECDSA = "ECDSA"; public SpongyCryptography() { super(new BouncyCastleProvider()); @@ -106,10 +104,7 @@ public class SpongyCryptography extends AbstractCryptography { KeySpec keySpec = new ECPublicKeySpec(Q, spec); PublicKey publicKey = KeyFactory.getInstance(ALGORITHM_ECDSA, provider).generatePublic(keySpec); - Signature sig = Signature.getInstance(ALGORITHM_ECDSA, provider); - sig.initVerify(publicKey); - sig.update(data); - return sig.verify(signature); + return doCheckSignature(data, signature, publicKey); } catch (GeneralSecurityException e) { throw new ApplicationException(e); } @@ -131,10 +126,7 @@ public class SpongyCryptography extends AbstractCryptography { java.security.PrivateKey privKey = KeyFactory.getInstance(ALGORITHM_ECDSA, provider) .generatePrivate(keySpec); - Signature sig = Signature.getInstance(ALGORITHM_ECDSA, provider); - sig.initSign(privKey); - sig.update(data); - return sig.sign(); + return doSign(data, privKey); } catch (GeneralSecurityException e) { throw new ApplicationException(e); } From 7185acbbad0411e1e55c7fd2f069818e175c0f76 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 2 Apr 2017 23:27:31 +0200 Subject: [PATCH 17/22] Bumped gradle version to 3.4.1 --- .../ch/dissem/gradle/GitFlowVersion.groovy | 4 +- gradle/wrapper/gradle-wrapper.jar | Bin 53637 -> 53324 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 46 ++++++++++-------- gradlew.bat | 8 +-- 5 files changed, 33 insertions(+), 29 deletions(-) diff --git a/buildSrc/src/main/groovy/ch/dissem/gradle/GitFlowVersion.groovy b/buildSrc/src/main/groovy/ch/dissem/gradle/GitFlowVersion.groovy index 869d57e..19c469a 100644 --- a/buildSrc/src/main/groovy/ch/dissem/gradle/GitFlowVersion.groovy +++ b/buildSrc/src/main/groovy/ch/dissem/gradle/GitFlowVersion.groovy @@ -50,8 +50,8 @@ class GitFlowVersion implements Plugin<Project> { project.ext.isRelease = isRelease(project) project.version = getVersion(project) - project.task('version') << { - println "Version deduced from git: '${project.version}'" + project.task('version') { + doLast { println "Version deduced from git: '${project.version}'" } } } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 05ef575b0cd0173fc735f2857ce4bd594ce4f6bd..3baa851b28c65f87dd36a6748e1a85cf360c1301 100644 GIT binary patch delta 5515 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 U^p_eN)%O;BQ^`^b*X=p~2l_@f>i_@% delta 5722 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ebcdff0..0879999 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Aug 12 11:52:04 CEST 2016 +#Sun Apr 02 23:05:26 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-bin.zip diff --git a/gradlew b/gradlew index 9d82f78..27309d9 100755 --- a/gradlew +++ b/gradlew @@ -6,12 +6,30 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/gradlew.bat b/gradlew.bat index aec9973..f6d5974 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args From dca0330a7cb06d55dd74c346cf4c30ae06ade3f5 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Mon, 3 Apr 2017 18:21:08 +0200 Subject: [PATCH 18/22] Bumped dependency versions --- build.gradle | 10 +++++++ core/build.gradle | 4 +-- .../utils/ConversationServiceTest.java | 2 +- .../bitmessage/utils/MessageMatchers.java | 27 ++++++------------- cryptography-bc/build.gradle | 2 +- cryptography-sc/build.gradle | 4 +-- demo/build.gradle | 12 ++++----- .../java/ch/dissem/bitmessage/SystemTest.java | 2 +- extensions/build.gradle | 4 +-- gradle/wrapper/gradle-wrapper.properties | 4 +-- networking/build.gradle | 6 ++--- repositories/build.gradle | 4 +-- wif/build.gradle | 2 +- 13 files changed, 41 insertions(+), 42 deletions(-) diff --git a/build.gradle b/build.gradle index 13b3ad0..6c8ea9e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,19 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.github.ben-manes:gradle-versions-plugin:0.14.0' + } +} + subprojects { apply plugin: 'java' apply plugin: 'maven' apply plugin: 'signing' apply plugin: 'jacoco' apply plugin: 'gitflow-version' + apply plugin: 'com.github.ben-manes.versions' sourceCompatibility = 1.7 group = 'ch.dissem.jabit' diff --git a/core/build.gradle b/core/build.gradle index 2fb78cd..785507e 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -24,10 +24,10 @@ artifacts { } dependencies { - compile 'org.slf4j:slf4j-api:1.7.12' + compile 'org.slf4j:slf4j-api:1.7.25' compile 'ch.dissem.msgpack:msgpack:1.0.0' testCompile 'junit:junit:4.12' testCompile 'org.hamcrest:hamcrest-library:1.3' - testCompile 'org.mockito:mockito-core:1.10.19' + testCompile 'org.mockito:mockito-core:2.7.21' testCompile project(':cryptography-bc') } diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/ConversationServiceTest.java b/core/src/test/java/ch/dissem/bitmessage/utils/ConversationServiceTest.java index d34a122..6543232 100644 --- a/core/src/test/java/ch/dissem/bitmessage/utils/ConversationServiceTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/utils/ConversationServiceTest.java @@ -32,7 +32,7 @@ import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; import static ch.dissem.bitmessage.utils.TestUtils.RANDOM; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/MessageMatchers.java b/core/src/test/java/ch/dissem/bitmessage/utils/MessageMatchers.java index e423d02..5be73ff 100644 --- a/core/src/test/java/ch/dissem/bitmessage/utils/MessageMatchers.java +++ b/core/src/test/java/ch/dissem/bitmessage/utils/MessageMatchers.java @@ -19,38 +19,27 @@ package ch.dissem.bitmessage.utils; import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.payload.ObjectType; -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.mockito.Matchers; +import org.mockito.ArgumentMatcher; +import org.mockito.ArgumentMatchers; /** * @author Christian Basler */ public class MessageMatchers { public static Plaintext plaintext(final Plaintext.Type type) { - return Matchers.argThat(new BaseMatcher<Plaintext>() { + return ArgumentMatchers.argThat(new ArgumentMatcher<Plaintext>() { @Override - public boolean matches(Object item) { - return item instanceof Plaintext && ((Plaintext) item).getType() == type; - } - - @Override - public void describeTo(Description description) { - description.appendText("type should be ").appendValue(type); + public boolean matches(Plaintext item) { + return item != null && item.getType() == type; } }); } public static ObjectMessage object(final ObjectType type) { - return Matchers.argThat(new BaseMatcher<ObjectMessage>() { + return ArgumentMatchers.argThat(new ArgumentMatcher<ObjectMessage>() { @Override - public boolean matches(Object item) { - return item instanceof ObjectMessage && ((ObjectMessage) item).getPayload().getType() == type; - } - - @Override - public void describeTo(Description description) { - description.appendText("payload type should be ").appendValue(type); + public boolean matches(ObjectMessage item) { + return item != null && item.getPayload().getType() == type; } }); } diff --git a/cryptography-bc/build.gradle b/cryptography-bc/build.gradle index b2ffaae..7dde574 100644 --- a/cryptography-bc/build.gradle +++ b/cryptography-bc/build.gradle @@ -14,5 +14,5 @@ dependencies { compile project(':core') compile 'org.bouncycastle:bcprov-jdk15on:1.56' testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:1.10.19' + testCompile 'org.mockito:mockito-core:2.7.21' } diff --git a/cryptography-sc/build.gradle b/cryptography-sc/build.gradle index cbfae46..c18f68d 100644 --- a/cryptography-sc/build.gradle +++ b/cryptography-sc/build.gradle @@ -12,7 +12,7 @@ uploadArchives { dependencies { compile project(':core') - compile 'com.madgag.spongycastle:prov:1.52.0.0' + compile 'com.madgag.spongycastle:prov:1.54.0.0' testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:1.10.19' + testCompile 'org.mockito:mockito-core:2.7.21' } diff --git a/demo/build.gradle b/demo/build.gradle index 79f5834..159dbc0 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -1,5 +1,5 @@ plugins { - id "us.kirchmeier.capsule" version "1.0-rc1" + id "us.kirchmeier.capsule" version "1.0.2" } uploadArchives { @@ -28,10 +28,10 @@ dependencies { compile project(':repositories') compile project(':cryptography-bc') compile project(':wif') - compile 'org.slf4j:slf4j-simple:1.7.12' - compile 'args4j:args4j:2.32' - compile 'com.h2database:h2:1.4.192' - compile 'org.apache.commons:commons-lang3:3.4' + compile 'org.slf4j:slf4j-simple:1.7.25' + compile 'args4j:args4j:2.33' + compile 'com.h2database:h2:1.4.194' + compile 'org.apache.commons:commons-lang3:3.5' testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:1.10.19' + testCompile 'org.mockito:mockito-core:2.7.21' } diff --git a/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java b/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java index 506045f..9f97432 100644 --- a/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java +++ b/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java @@ -28,7 +28,7 @@ import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK; import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; /** * @author Christian Basler diff --git a/extensions/build.gradle b/extensions/build.gradle index 42b175b..1b3cc8b 100644 --- a/extensions/build.gradle +++ b/extensions/build.gradle @@ -29,8 +29,8 @@ uploadArchives { dependencies { compile project(':core') testCompile 'junit:junit:4.12' - testCompile 'org.slf4j:slf4j-simple:1.7.12' - testCompile 'org.mockito:mockito-core:1.10.19' + testCompile 'org.slf4j:slf4j-simple:1.7.25' + testCompile 'org.mockito:mockito-core:2.7.21' testCompile project(path: ':core', configuration: 'testArtifacts') testCompile project(':cryptography-bc') } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0879999..6f15c32 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun Apr 02 23:05:26 CEST 2017 +#Mon Apr 03 17:55:37 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip diff --git a/networking/build.gradle b/networking/build.gradle index 49cafa5..b01eb40 100644 --- a/networking/build.gradle +++ b/networking/build.gradle @@ -13,8 +13,8 @@ uploadArchives { dependencies { compile project(':core') testCompile 'junit:junit:4.12' - testCompile 'org.slf4j:slf4j-simple:1.7.12' - testCompile 'org.mockito:mockito-core:1.10.19' + testCompile 'org.slf4j:slf4j-simple:1.7.25' + testCompile 'org.mockito:mockito-core:2.7.21' testCompile project(path: ':core', configuration: 'testArtifacts') testCompile project(':cryptography-bc') -} \ No newline at end of file +} diff --git a/repositories/build.gradle b/repositories/build.gradle index 664279a..79b8578 100644 --- a/repositories/build.gradle +++ b/repositories/build.gradle @@ -14,10 +14,10 @@ sourceCompatibility = 1.8 dependencies { compile project(':core') - compile 'org.flywaydb:flyway-core:4.0.3' + compile 'org.flywaydb:flyway-core:4.1.2' testCompile 'junit:junit:4.12' testCompile 'com.h2database:h2:1.4.194' - testCompile 'org.mockito:mockito-core:1.10.19' + testCompile 'org.mockito:mockito-core:2.7.21' testCompile project(path: ':core', configuration: 'testArtifacts') testCompile project(':cryptography-bc') } diff --git a/wif/build.gradle b/wif/build.gradle index 0c1ae14..a466f69 100644 --- a/wif/build.gradle +++ b/wif/build.gradle @@ -14,6 +14,6 @@ dependencies { compile project(':core') compile 'org.ini4j:ini4j:0.5.4' testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:1.10.19' + testCompile 'org.mockito:mockito-core:2.7.21' testCompile project(':cryptography-bc') } From 95c9be4d1c0e7e5a7eb87ffd088b4c1ba676af70 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Tue, 4 Apr 2017 07:31:37 +0200 Subject: [PATCH 19/22] A slightly more elegant way to define the plugin dependency --- build.gradle | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 6c8ea9e..78bf578 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,5 @@ -buildscript { - repositories { - jcenter() - } - dependencies { - classpath 'com.github.ben-manes:gradle-versions-plugin:0.14.0' - } +plugins { + id 'com.github.ben-manes.versions' version '0.14.0' } subprojects { From f50d7445c125a998a888d6a279b10e2d79f421f2 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Wed, 12 Apr 2017 17:12:43 +0200 Subject: [PATCH 20/22] Validate TTL: it must always be more than zero and less than 28 days --- .../java/ch/dissem/bitmessage/utils/TTL.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/TTL.java b/core/src/main/java/ch/dissem/bitmessage/utils/TTL.java index 17e31e5..d1feb8a 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/TTL.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/TTL.java @@ -3,9 +3,8 @@ package ch.dissem.bitmessage.utils; import static ch.dissem.bitmessage.utils.UnixTime.DAY; /** - * Stores times to live for different object types. Usually this shouldn't be messed with, - * but for tests it might be a good idea to reduce it to a minimum, and on mobile clients - * you might want to optimize it as well. + * Stores times to live in seconds for different object types. Usually this shouldn't be messed with, but for tests + * it might be a good idea to reduce it to a minimum, and on mobile clients you might want to optimize it as well. * * @author Christian Basler */ @@ -19,7 +18,7 @@ public class TTL { } public static void msg(long msg) { - TTL.msg = msg; + TTL.msg = validate(msg); } public static long getpubkey() { @@ -27,7 +26,7 @@ public class TTL { } public static void getpubkey(long getpubkey) { - TTL.getpubkey = getpubkey; + TTL.getpubkey = validate(getpubkey); } public static long pubkey() { @@ -35,6 +34,11 @@ public class TTL { } public static void pubkey(long pubkey) { - TTL.pubkey = pubkey; + TTL.pubkey = validate(pubkey); + } + + private static long validate(long ttl) { + if (ttl < 0 || ttl > 28 * DAY) throw new IllegalArgumentException("TTL must be between 1 and 28 days"); + return ttl; } } From e5c956c6e5188a38d435360df943c0602b70ae6b Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Wed, 12 Apr 2017 17:18:09 +0200 Subject: [PATCH 21/22] Minor improvements --- .../dissem/bitmessage/BitmessageContext.java | 21 +---------- .../dissem/bitmessage/ProofOfWorkService.java | 2 +- .../ch/dissem/bitmessage/entity/Version.java | 37 ++++++++++++++++++- .../entity/valueobject/NetworkAddress.java | 24 ++++++++---- .../bitmessage/factory/V3MessageFactory.java | 2 + 5 files changed, 57 insertions(+), 29 deletions(-) diff --git a/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java b/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java index 8d3c0d7..29639e7 100644 --- a/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java +++ b/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java @@ -25,7 +25,6 @@ import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.ports.*; import ch.dissem.bitmessage.utils.Property; -import ch.dissem.bitmessage.utils.TTL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,7 +38,8 @@ import static ch.dissem.bitmessage.InternalContext.NETWORK_EXTRA_BYTES; import static ch.dissem.bitmessage.InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE; import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST; import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; -import static ch.dissem.bitmessage.utils.UnixTime.*; +import static ch.dissem.bitmessage.utils.UnixTime.HOUR; +import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; /** * <p>Use this class if you want to create a Bitmessage client.</p> @@ -404,23 +404,6 @@ public class BitmessageContext { return this; } - /** - * Time to live in seconds for public keys the client sends. Defaults to the maximum of 28 days, - * but on weak devices smaller values might be desirable. - * <p> - * Please be aware that this might cause some problems where you can't receive a message (the - * sender can't receive your public key) in some special situations. Also note that it's probably - * not a good idea to set it too low. - * </p> - * - * @deprecated use {@link TTL#pubkey(long)} instead. - */ - public Builder pubkeyTTL(long days) { - if (days < 0 || days > 28 * DAY) throw new IllegalArgumentException("TTL must be between 1 and 28 days"); - TTL.pubkey(days); - return this; - } - public BitmessageContext build() { nonNull("inventory", inventory); nonNull("nodeRegistry", nodeRegistry); diff --git a/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java b/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java index 7c72bea..14e7c8a 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java +++ b/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java @@ -81,7 +81,7 @@ public class ProofOfWorkService implements ProofOfWorkEngine.Callback, InternalC public void onNonceCalculated(byte[] initialHash, byte[] nonce) { Item item = powRepo.getItem(initialHash); if (item.message == null) { - ObjectMessage object = powRepo.getItem(initialHash).object; + ObjectMessage object = item.object; object.setNonce(nonce); Plaintext plaintext = messageRepo.getMessage(initialHash); if (plaintext != null) { diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/Version.java b/core/src/main/java/ch/dissem/bitmessage/entity/Version.java index 4d0fd05..3a44366 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Version.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Version.java @@ -20,6 +20,8 @@ import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; import ch.dissem.bitmessage.utils.Encode; import ch.dissem.bitmessage.utils.UnixTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.OutputStream; @@ -30,6 +32,7 @@ import java.nio.ByteBuffer; */ public class Version implements MessagePayload { private static final long serialVersionUID = 7219240857343176567L; + private static final Logger LOG = LoggerFactory.getLogger(Version.class); /** * Identifies protocol version being used by the node. Should equal 3. Nodes should disconnect if the remote node's @@ -93,6 +96,10 @@ public class Version implements MessagePayload { return services; } + public boolean provides(Service service) { + return service != null && service.isEnabled(services); + } + public long getTimestamp() { return timestamp; } @@ -159,7 +166,7 @@ public class Version implements MessagePayload { public Builder defaults(long clientNonce) { version = BitmessageContext.CURRENT_VERSION; - services = 1; + services = Service.getServiceFlag(Service.NODE_NETWORK); timestamp = UnixTime.now(); userAgent = "/Jabit:0.0.1/"; streamNumbers = new long[]{1}; @@ -172,6 +179,11 @@ public class Version implements MessagePayload { return this; } + public Builder services(Service... services) { + this.services = Service.getServiceFlag(services); + return this; + } + public Builder services(long services) { this.services = services; return this; @@ -211,4 +223,27 @@ public class Version implements MessagePayload { return new Version(this); } } + + public enum Service { + NODE_NETWORK(1); +// TODO: NODE_SSL(2); + + long flag; + + Service(long flag) { + this.flag = flag; + } + + public boolean isEnabled(long flag) { + return (flag & this.flag) != 0; + } + + public static long getServiceFlag(Service... services) { + long flag = 0; + for (Service service : services) { + flag |= service.flag; + } + return flag; + } + } } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.java index 94bc7c0..fe3bfcb 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/NetworkAddress.java @@ -17,6 +17,7 @@ package ch.dissem.bitmessage.entity.valueobject; import ch.dissem.bitmessage.entity.Streamable; +import ch.dissem.bitmessage.entity.Version; import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.utils.Encode; import ch.dissem.bitmessage.utils.UnixTime; @@ -75,6 +76,13 @@ public class NetworkAddress implements Streamable { return services; } + public boolean provides(Version.Service service) { + if (service == null) { + return false; + } + return service.isEnabled(services); + } + public long getStream() { return stream; } @@ -194,20 +202,20 @@ public class NetworkAddress implements Streamable { int p08, int p09, int p10, int p11, int p12, int p13, int p14, int p15) { this.ipv6 = new byte[]{ - (byte) p00, (byte) p01, (byte) p02, (byte) p03, - (byte) p04, (byte) p05, (byte) p06, (byte) p07, - (byte) p08, (byte) p09, (byte) p10, (byte) p11, - (byte) p12, (byte) p13, (byte) p14, (byte) p15 + (byte) p00, (byte) p01, (byte) p02, (byte) p03, + (byte) p04, (byte) p05, (byte) p06, (byte) p07, + (byte) p08, (byte) p09, (byte) p10, (byte) p11, + (byte) p12, (byte) p13, (byte) p14, (byte) p15 }; return this; } public Builder ipv4(int p00, int p01, int p02, int p03) { this.ipv6 = new byte[]{ - (byte) 0, (byte) 0, (byte) 0x00, (byte) 0x00, - (byte) 0, (byte) 0, (byte) 0x00, (byte) 0x00, - (byte) 0, (byte) 0, (byte) 0xff, (byte) 0xff, - (byte) p00, (byte) p01, (byte) p02, (byte) p03 + (byte) 0, (byte) 0, (byte) 0x00, (byte) 0x00, + (byte) 0, (byte) 0, (byte) 0x00, (byte) 0x00, + (byte) 0, (byte) 0, (byte) 0xff, (byte) 0xff, + (byte) p00, (byte) p01, (byte) p02, (byte) p03 }; return this; } diff --git a/core/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java b/core/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java index 7b27d13..a702e16 100644 --- a/core/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java +++ b/core/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java @@ -24,6 +24,7 @@ import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; import ch.dissem.bitmessage.exception.NodeException; import ch.dissem.bitmessage.utils.AccessCounter; import ch.dissem.bitmessage.utils.Decode; +import ch.dissem.bitmessage.utils.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -103,6 +104,7 @@ class V3MessageFactory { payload = Factory.getObjectPayload(objectType, version, stream, dataStream, data.length); } catch (Exception e) { LOG.trace("Could not parse object payload - using generic payload instead", e); + LOG.info(Strings.hex(data).toString()); payload = new GenericPayload(version, stream, data); } From 2ae1e561d810fb2141ac77a9d8a54ed19190f549 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 4 May 2017 07:24:26 +0200 Subject: [PATCH 22/22] Minor improvements for logging and debugging --- .../java/ch/dissem/bitmessage/utils/TTL.java | 2 +- .../networking/nio/NioNetworkHandler.java | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/TTL.java b/core/src/main/java/ch/dissem/bitmessage/utils/TTL.java index d1feb8a..c4fab9c 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/TTL.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/TTL.java @@ -38,7 +38,7 @@ public class TTL { } private static long validate(long ttl) { - if (ttl < 0 || ttl > 28 * DAY) throw new IllegalArgumentException("TTL must be between 1 and 28 days"); + if (ttl < 0 || ttl > 28 * DAY) throw new IllegalArgumentException("TTL must be between 0 seconds and 28 days"); return ttl; } } diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/nio/NioNetworkHandler.java b/networking/src/main/java/ch/dissem/bitmessage/networking/nio/NioNetworkHandler.java index b79174b..04e6d4d 100644 --- a/networking/src/main/java/ch/dissem/bitmessage/networking/nio/NioNetworkHandler.java +++ b/networking/src/main/java/ch/dissem/bitmessage/networking/nio/NioNetworkHandler.java @@ -254,7 +254,7 @@ public class NioNetworkHandler implements NetworkHandler, InternalContext.Contex } } } catch (CancelledKeyException e) { - LOG.error(e.getMessage(), e); + LOG.debug(e.getMessage(), e); } } else { // handle read/write @@ -287,11 +287,15 @@ public class NioNetworkHandler implements NetworkHandler, InternalContext.Contex } // set interest ops for (Map.Entry<ConnectionInfo, SelectionKey> e : connections.entrySet()) { - if (e.getValue().isValid() - && (e.getValue().interestOps() & OP_WRITE) == 0 - && (e.getValue().interestOps() & OP_CONNECT) == 0 - && !e.getKey().getSendingQueue().isEmpty()) { - e.getValue().interestOps(OP_READ | OP_WRITE); + try { + if (e.getValue().isValid() + && (e.getValue().interestOps() & OP_WRITE) == 0 + && (e.getValue().interestOps() & OP_CONNECT) == 0 + && !e.getKey().getSendingQueue().isEmpty()) { + e.getValue().interestOps(OP_READ | OP_WRITE); + } + } catch (CancelledKeyException x) { + e.getKey().disconnect(); } } // start new connections