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/build.gradle b/build.gradle index 311c3f9..78bf578 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,21 @@ +plugins { + id 'com.github.ben-manes.versions' version '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' repositories { mavenCentral() + maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } } test { 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.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/core/build.gradle b/core/build.gradle index 73f8fdf..785507e 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -24,9 +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/main/java/ch/dissem/bitmessage/BitmessageContext.java b/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java index 9e155ad..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; /** *

Use this class if you want to create a Bitmessage client.

@@ -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() { @@ -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); + } } } @@ -401,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. - *

- * 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. - *

- * - * @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); @@ -435,8 +421,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; } }; } 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/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) { 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/Plaintext.java b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java index 710e041..141edfc 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java @@ -18,9 +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.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.*; @@ -29,6 +33,8 @@ 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,8 @@ 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; private InventoryVector inventoryVector; @@ -81,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 { @@ -143,6 +152,10 @@ public class Plaintext implements Streamable { return labels; } + public Encoding getEncoding() { + return Encoding.fromCode(encoding); + } + public long getStream() { return from.getStream(); } @@ -167,12 +180,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()); @@ -202,12 +226,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()); @@ -299,7 +334,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 (Message.TYPE.equals(getExtendedData().getType())) { + return ((Message) extendedData.getContent()).getSubject(); + } else { + return null; + } + } else if (encoding == SIMPLE.code) { return firstLine.substring("Subject:".length()).trim(); } else if (firstLine.length() > 50) { return firstLine.substring(0, 50).trim() + "..."; @@ -309,17 +350,65 @@ 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 (Message.TYPE.equals(getExtendedData().getType())) { + return ((Message) extendedData.getContent()).getBody(); + } else { + return null; + } + } 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); } - 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 = ExtendedEncodingFactory.getInstance().unzip(message); + } + return extendedData; + } + + @SuppressWarnings("unchecked") + public T getExtendedData(Class type) { + ExtendedEncoding extendedData = getExtendedData(); + if (extendedData == null) { + return null; + } + if (type == null || type.isInstance(extendedData.getContent())) { + return (T) extendedData.getContent(); + } + return null; + } + + public List getParents() { + if (getExtendedData() != null && Message.TYPE.equals(getExtendedData().getType())) { + return ((Message) extendedData.getContent()).getParents(); + } else { + return Collections.emptyList(); + } + } + + public List getFiles() { + if (Message.TYPE.equals(getExtendedData().getType())) { + return ((Message) extendedData.getContent()).getFiles(); + } else { + return Collections.emptyList(); + } + } + + public UUID getConversationId() { + return conversationId; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -350,9 +439,7 @@ public class Plaintext implements Streamable { public void addLabels(Collection