diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..656384c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_size = 4 diff --git a/.gitignore b/.gitignore index fd29962..98d31cd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ ### Gradle ### .gradle build/ +classes/ # Ignore Gradle GUI config gradle-app.setting diff --git a/.travis.yml b/.travis.yml index 9bcf999..aacb38f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,10 @@ language: java +sudo: false # faster builds jdk: - oraclejdk8 + +before_install: + - pip install --user codecov + +after_success: + - codecov diff --git a/README.md b/README.md index 16a3c03..0f536d9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,33 @@ -Jabit [![Build Status](https://travis-ci.org/Dissem/Jabit.svg?branch=master)](https://travis-ci.org/Dissem/Jabit) +Jabit ===== +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/ch.dissem.jabit/jabit-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/ch.dissem.jabit/jabit-core) +[![Javadoc](https://javadoc-emblem.rhcloud.com/doc/ch.dissem.jabit/jabit-core/badge.svg)](http://www.javadoc.io/doc/ch.dissem.jabit/jabit-core) +[![Apache 2](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](https://raw.githubusercontent.com/Dissem/Jabit/master/LICENSE) +[![Visit our IRC channel](https://img.shields.io/badge/irc-%23jabit-blue.svg)](https://kiwiirc.com/client/irc.freenode.net/#jabit) -A Java implementation for the Bitmessage protocol. To build, use command `gradle build` or `./gradlew build`. +A Java implementation for the Bitmessage protocol. To build, use command `./gradlew build`. 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!_ + +#### Master +[![Build Status](https://travis-ci.org/Dissem/Jabit.svg?branch=master)](https://travis-ci.org/Dissem/Jabit) +[![Code Quality](https://img.shields.io/codacy/e9938d2adbb74a0db553115bef692ff3/master.svg)](https://www.codacy.com/app/chrigu-meyer/Jabit/dashboard?bid=3144281) +[![Test Coverage](https://codecov.io/github/Dissem/Jabit/coverage.svg?branch=master)](https://codecov.io/github/Dissem/Jabit?branch=master) + +#### Develop +[![Build Status](https://travis-ci.org/Dissem/Jabit.svg?branch=develop)](https://travis-ci.org/Dissem/Jabit?branch=develop) +[![Code Quality](https://img.shields.io/codacy/e9938d2adbb74a0db553115bef692ff3/develop.svg)](https://www.codacy.com/app/chrigu-meyer/Jabit/dashboard?bid=3144279) +[![Test Coverage](https://codecov.io/github/Dissem/Jabit/coverage.svg?branch=develop)](https://codecov.io/github/Dissem/Jabit?branch=develop) + +Upgrading +--------- + +Please be aware that Version 2.0.0 has some breaking changes, most notably in the repository implementations -- please take special care when upgrading them. If you don't implement your own repositories, you should be able to quickly find and fix any compilation errors caused by the few other breaking changes. + +There is also a new network handler which comes highly recommended. If you're having any network problems, please make sure you use `NioNetworkHandler` instead of the now deprecated `DefaultNetworkHandler`. + Security -------- @@ -27,19 +50,23 @@ Basically, everything needed for a working Bitmessage client is there: Setup ----- +It is recommended to define the version like this: +```Gradle +ext.jabitVersion = '2.0.0' +``` Add Jabit as Gradle dependency: ```Gradle -compile 'ch.dissem.jabit:jabit-core:1.0.0' +compile "ch.dissem.jabit:jabit-core:$jabitVersion" ``` Unless you want to implement your own, also add the following: ```Gradle -compile 'ch.dissem.jabit:jabit-networking:1.0.0' -compile 'ch.dissem.jabit:jabit-repositories:1.0.0' -compile 'ch.dissem.jabit:jabit-cryptography-bouncy:1.0.0' +compile "ch.dissem.jabit:jabit-networking:$jabitVersion" +compile "ch.dissem.jabit:jabit-repositories:$jabitVersion" +compile "ch.dissem.jabit:jabit-cryptography-bouncy:$jabitVersion" ``` And if you want to import from or export to the Wallet Import Format (used by PyBitmessage) you might also want to add: ```Gradle -compile 'ch.dissem.jabit:jabit-wif:1.0.0' +compile "ch.dissem.jabit:jabit-wif:$jabitVersion" ``` For Android clients use `jabit-cryptography-spongy` instead of `jabit-cryptography-bouncy`. @@ -54,20 +81,26 @@ BitmessageContext ctx = new BitmessageContext.Builder() .addressRepo(new JdbcAddressRepository(jdbcConfig)) .inventory(new JdbcInventory(jdbcConfig)) .messageRepo(new JdbcMessageRepository(jdbcConfig)) - .nodeRegistry(new MemoryNodeRegistry()) - .networkHandler(new NetworkNode()) + .powRepo(new JdbcProofOfWorkRepository(jdbcConfig)) + .nodeRegistry(new JdbcNodeRegistry(jdbcConfig)) + .networkHandler(new NioNetworkHandler()) .cryptography(new BouncyCryptography()) + .listener(System.out::println) .build(); ``` -This creates a simple context using a H2 database that will be created in the user's home directory. Next you'll need to -start the context and decide what happens if a message arrives: +This creates a simple context using a H2 database that will be created in the user's home directory. In the listener you decide what happens when a message arrives. If you can't use lambdas, you may instead write ```Java -ctx.startup(new BitmessageContext.Listener() { - @Override - public void receive(Plaintext plaintext) { - // TODO: Notify the user - } -}); + .listener(new BitmessageContext.Listener() { + @Override + public void receive(Plaintext plaintext) { + // TODO: Notify the user + } + }) +``` + +Next you'll need to start the context: +```Java +ctx.startup() ``` Then you might want to create an identity ```Java @@ -83,3 +116,22 @@ to which you can send some messages ```Java ctx.send(identity, contact, "Test", "Hello Chris, this is a message."); ``` + +### Housekeeping + +As Bitmessage stores all currently valid messages, we'll need to delete expired objects from time to time: +```Java +ctx.cleanup(); +``` +If the client runs all the time, it might be a good idea to do this daily or at least weekly. Otherwise, you might just want to clean up on shutdown. + +Also, if some messages weren't acknowledged when it expired, they can be resent: +```Java +ctx.resendUnacknowledgedMessages(); +``` +This could be triggered periodically, or manually by the user. Please be aware that _if_ there is a message to resend, proof of work needs to be calculated, so to not annoy your users you might not want to trigger it on shutdown. As the client might have been offline for some time, it might as well be wise to wait until it caught up downloading new messages before resending those messages, after all they might be acknowledged by now. + +There probably won't happen extremely bad things if you don't - at least not more than otherwise - but you can properly shutdown the network connection by calling +```Java +ctx.shutdown(); +``` diff --git a/build.gradle b/build.gradle index 0f6cf26..311c3f9 100644 --- a/build.gradle +++ b/build.gradle @@ -2,12 +2,11 @@ subprojects { apply plugin: 'java' apply plugin: 'maven' apply plugin: 'signing' + apply plugin: 'jacoco' + apply plugin: 'gitflow-version' sourceCompatibility = 1.7 group = 'ch.dissem.jabit' - version = '1.0.1' - - ext.isReleaseVersion = !version.endsWith("SNAPSHOT") repositories { mavenCentral() @@ -34,7 +33,7 @@ subprojects { } signing { - required { isReleaseVersion && project.getProperties().get("signing.keyId")?.length() > 0 } + required { isRelease && project.getProperties().get("signing.keyId")?.length() > 0 } sign configurations.archives } @@ -79,4 +78,13 @@ subprojects { } } } -} \ No newline at end of file + + jacocoTestReport { + reports { + xml.enabled = true + html.enabled = true + } + } + + check.dependsOn jacocoTestReport +} diff --git a/buildSrc/src/main/groovy/ch/dissem/gradle/GitFlowVersion.groovy b/buildSrc/src/main/groovy/ch/dissem/gradle/GitFlowVersion.groovy new file mode 100644 index 0000000..869d57e --- /dev/null +++ b/buildSrc/src/main/groovy/ch/dissem/gradle/GitFlowVersion.groovy @@ -0,0 +1,57 @@ +package ch.dissem.gradle + +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** + * Sets the version as follows: + * + */ +class GitFlowVersion implements Plugin { + def getBranch(Project project) { + def stdout = new ByteArrayOutputStream() + project.exec { + commandLine 'git', 'rev-parse', '--abbrev-ref', 'HEAD' + standardOutput = stdout + } + return stdout.toString().trim() + } + + def getTag(Project project) { + def stdout = new ByteArrayOutputStream() + project.exec { + commandLine 'git', 'describe', '--abbrev=0' + standardOutput = stdout + } + return stdout.toString().trim() + } + + def isRelease(Project project) { + return "master" == getBranch(project); + } + + def getVersion(Project project) { + if (project.ext.isRelease) { + return getTag(project) + } else { + def branch = getBranch(project) + if ("develop" == branch) { + return "development-SNAPSHOT" + } + return branch.replaceAll("/", "-") + "-SNAPSHOT" + } + } + + @Override + void apply(Project project) { + project.ext.isRelease = isRelease(project) + project.version = getVersion(project) + + project.task('version') << { + println "Version deduced from git: '${project.version}'" + } + } +} diff --git a/buildSrc/src/main/resources/META-INF/gradle-plugins/gitflow-version.properties b/buildSrc/src/main/resources/META-INF/gradle-plugins/gitflow-version.properties new file mode 100644 index 0000000..1fb4f78 --- /dev/null +++ b/buildSrc/src/main/resources/META-INF/gradle-plugins/gitflow-version.properties @@ -0,0 +1 @@ +implementation-class=ch.dissem.gradle.GitFlowVersion diff --git a/core/build.gradle b/core/build.gradle index 8754cb3..73f8fdf 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -25,7 +25,8 @@ artifacts { dependencies { compile 'org.slf4j:slf4j-api:1.7.12' - testCompile 'junit:junit:4.11' + testCompile 'junit:junit:4.12' + testCompile 'org.hamcrest:hamcrest-library:1.3' testCompile 'org.mockito:mockito-core:1.10.19' 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 c99acfb..9e155ad 100644 --- a/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java +++ b/core/src/main/java/ch/dissem/bitmessage/BitmessageContext.java @@ -18,14 +18,11 @@ package ch.dissem.bitmessage; import ch.dissem.bitmessage.entity.*; import ch.dissem.bitmessage.entity.payload.Broadcast; -import ch.dissem.bitmessage.entity.payload.Msg; -import ch.dissem.bitmessage.entity.payload.ObjectPayload; import ch.dissem.bitmessage.entity.payload.ObjectType; import ch.dissem.bitmessage.entity.payload.Pubkey.Feature; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; 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; @@ -33,11 +30,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.InetAddress; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.*; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; -import static ch.dissem.bitmessage.entity.Plaintext.Status.*; +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.*; @@ -60,32 +59,20 @@ public class BitmessageContext { public static final int CURRENT_VERSION = 3; private final static Logger LOG = LoggerFactory.getLogger(BitmessageContext.class); - private final ExecutorService pool; - private final InternalContext ctx; - private final Listener listener; - private final NetworkHandler.MessageListener networkListener; + private final Labeler labeler; private final boolean sendPubkeyOnIdentityCreation; private BitmessageContext(Builder builder) { + if (builder.listener instanceof Listener.WithContext) { + ((Listener.WithContext) builder.listener).setContext(this); + } ctx = new InternalContext(builder); - listener = builder.listener; - networkListener = new DefaultMessageListener(ctx, listener); - - // As this thread is used for parts that do POW, which itself uses parallel threads, only - // one should be executed at any time. - pool = Executors.newFixedThreadPool(1); - + labeler = builder.labeler; + ctx.getProofOfWorkService().doMissingProofOfWork(30_000); // TODO: this should be configurable sendPubkeyOnIdentityCreation = builder.sendPubkeyOnIdentityCreation; - - new Timer().schedule(new TimerTask() { - @Override - public void run() { - ctx.getProofOfWorkService().doMissingProofOfWork(); - } - }, 30_000); // After 30 seconds } public AddressRepository addresses() { @@ -96,36 +83,56 @@ public class BitmessageContext { return ctx.getMessageRepository(); } + public Labeler labeler() { + return labeler; + } + public BitmessageAddress createIdentity(boolean shorter, Feature... features) { final BitmessageAddress identity = new BitmessageAddress(new PrivateKey( - shorter, - ctx.getStreams()[0], - ctx.getNetworkNonceTrialsPerByte(), - ctx.getNetworkExtraBytes(), - features + shorter, + ctx.getStreams()[0], + NETWORK_NONCE_TRIALS_PER_BYTE, + NETWORK_EXTRA_BYTES, + features )); ctx.getAddressRepository().save(identity); if (sendPubkeyOnIdentityCreation) { - pool.submit(new Runnable() { - @Override - public void run() { - ctx.sendPubkey(identity, identity.getStream()); - } - }); + ctx.sendPubkey(identity, identity.getStream()); } return identity; } - public void addDistributedMailingList(String address, String alias) { - // TODO - throw new RuntimeException("not implemented"); + public BitmessageAddress joinChan(String passphrase, String address) { + BitmessageAddress chan = BitmessageAddress.chan(address, passphrase); + chan.setAlias(passphrase); + ctx.getAddressRepository().save(chan); + return chan; + } + + public BitmessageAddress createChan(String passphrase) { + // FIXME: hardcoded stream number + BitmessageAddress chan = BitmessageAddress.chan(1, passphrase); + ctx.getAddressRepository().save(chan); + return chan; + } + + public List createDeterministicAddresses( + String passphrase, int numberOfAddresses, long version, long stream, boolean shorter) { + List result = BitmessageAddress.deterministic( + passphrase, numberOfAddresses, version, stream, shorter); + for (int i = 0; i < result.size(); i++) { + BitmessageAddress address = result.get(i); + address.setAlias("deterministic (" + (i + 1) + ")"); + ctx.getAddressRepository().save(address); + } + return result; } public void broadcast(final BitmessageAddress from, final String subject, final String message) { Plaintext msg = new Plaintext.Builder(BROADCAST) - .from(from) - .message(subject, message) - .build(); + .from(from) + .message(subject, message) + .build(); send(msg); } @@ -134,11 +141,10 @@ public class BitmessageContext { throw new IllegalArgumentException("'From' must be an identity, i.e. have a private key."); } Plaintext msg = new Plaintext.Builder(MSG) - .from(from) - .to(to) - .message(subject, message) - .labels(messages().getLabels(Label.Type.SENT)) - .build(); + .from(from) + .to(to) + .message(subject, message) + .build(); send(msg); } @@ -146,41 +152,35 @@ public class BitmessageContext { if (msg.getFrom() == null || msg.getFrom().getPrivateKey() == null) { throw new IllegalArgumentException("'From' must be an identity, i.e. have a private key."); } - pool.submit(new Runnable() { - @Override - public void run() { - BitmessageAddress to = msg.getTo(); - if (to != null) { - if (to.getPubkey() == null) { - LOG.info("Public key is missing from recipient. Requesting."); - ctx.requestPubkey(to); - } - if (to.getPubkey() == null) { - msg.setStatus(PUBKEY_REQUESTED); - msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.OUTBOX)); - ctx.getMessageRepository().save(msg); - } - } - if (to == null || to.getPubkey() != null) { - LOG.info("Sending message."); - msg.setStatus(DOING_PROOF_OF_WORK); - ctx.getMessageRepository().save(msg); - ctx.send( - msg.getFrom(), - to, - new Msg(msg), - TTL.msg() - ); - msg.setStatus(SENT); - msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.SENT)); - ctx.getMessageRepository().save(msg); - } + labeler().markAsSending(msg); + BitmessageAddress to = msg.getTo(); + if (to != null) { + if (to.getPubkey() == null) { + LOG.info("Public key is missing from recipient. Requesting."); + ctx.requestPubkey(to); } - }); + if (to.getPubkey() == null) { + ctx.getMessageRepository().save(msg); + } + } + if (to == null || to.getPubkey() != null) { + LOG.info("Sending message."); + ctx.getMessageRepository().save(msg); + if (msg.getType() == MSG) { + ctx.send(msg); + } else { + ctx.send( + msg.getFrom(), + to, + Factory.getBroadcast(msg), + msg.getTTL() + ); + } + } } public void startup() { - ctx.getNetworkHandler().start(networkListener); + ctx.getNetworkHandler().start(); } public void shutdown() { @@ -195,7 +195,7 @@ public class BitmessageContext { * @param wait waits for the synchronization thread to finish */ public void synchronize(InetAddress host, int port, long timeoutInSeconds, boolean wait) { - Future future = ctx.getNetworkHandler().synchronize(host, port, networkListener, timeoutInSeconds); + Future future = ctx.getNetworkHandler().synchronize(host, port, timeoutInSeconds); if (wait) { try { future.get(); @@ -221,10 +221,32 @@ public class BitmessageContext { return ctx.getNetworkHandler().send(server, port, request); } + /** + * Removes expired objects from the inventory. You should call this method regularly, + * e.g. daily and on each shutdown. + */ public void cleanup() { ctx.getInventory().cleanup(); } + /** + * Sends messages again whose time to live expired without being acknowledged. (And whose + * recipient is expected to send acknowledgements. + *

+ * You should call this method regularly, but be aware of the following: + *

    + *
  • As messages might be sent, POW will be done. It is therefore not advised to + * call it on shutdown.
  • + *
  • It shouldn't be called right after startup, as it's possible the missing + * acknowledgement was sent while the client was offline.
  • + *
  • Other than that, the call isn't expensive as long as there is no message + * to send, so it might be a good idea to just call it every few minutes.
  • + *
+ */ + public void resendUnacknowledgedMessages() { + ctx.resendUnacknowledged(); + } + public boolean isRunning() { return ctx.getNetworkHandler().isRunning(); } @@ -247,7 +269,9 @@ public class BitmessageContext { try { Broadcast broadcast = (Broadcast) object.getPayload(); broadcast.decrypt(address); - listener.receive(broadcast.getPlaintext()); + // This decrypts it twice, but on the other hand it doesn't try to decrypt the objects with + // other subscriptions and the interface stays as simple as possible. + ctx.getNetworkListener().receive(object); } catch (DecryptionFailedException ignore) { } catch (Exception e) { LOG.debug(e.getMessage(), e); @@ -257,7 +281,8 @@ public class BitmessageContext { public Property status() { return new Property("status", null, - ctx.getNetworkHandler().getNetworkStatus() + ctx.getNetworkHandler().getNetworkStatus(), + new Property("unacknowledged", ctx.getMessageRepository().findMessagesToResend().size()) ); } @@ -271,6 +296,13 @@ public class BitmessageContext { public interface Listener { void receive(Plaintext plaintext); + + /** + * A message listener that needs a {@link BitmessageContext}, i.e. for implementing some sort of chat bot. + */ + interface WithContext extends Listener { + void setContext(BitmessageContext ctx); + } } public static final class Builder { @@ -283,16 +315,13 @@ public class BitmessageContext { ProofOfWorkRepository proofOfWorkRepository; ProofOfWorkEngine proofOfWorkEngine; Cryptography cryptography; - MessageCallback messageCallback; CustomCommandHandler customCommandHandler; + Labeler labeler; Listener listener; int connectionLimit = 150; long connectionTTL = 30 * MINUTE; boolean sendPubkeyOnIdentityCreation = true; - public Builder() { - } - public Builder port(int port) { this.port = port; return this; @@ -333,11 +362,6 @@ public class BitmessageContext { return this; } - public Builder messageCallback(MessageCallback callback) { - this.messageCallback = callback; - return this; - } - public Builder customCommandHandler(CustomCommandHandler handler) { this.customCommandHandler = handler; return this; @@ -348,6 +372,11 @@ public class BitmessageContext { return this; } + public Builder labeler(Labeler labeler) { + this.labeler = labeler; + return this; + } + public Builder listener(Listener listener) { this.listener = listener; return this; @@ -380,6 +409,8 @@ public class BitmessageContext { * 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"); @@ -397,30 +428,15 @@ public class BitmessageContext { if (proofOfWorkEngine == null) { proofOfWorkEngine = new MultiThreadedPOWEngine(); } - if (messageCallback == null) { - messageCallback = new MessageCallback() { - @Override - public void proofOfWorkStarted(ObjectPayload message) { - } - - @Override - public void proofOfWorkCompleted(ObjectPayload message) { - } - - @Override - public void messageOffered(ObjectPayload message, InventoryVector iv) { - } - - @Override - public void messageAcknowledged(InventoryVector iv) { - } - }; + if (labeler == null) { + labeler = new DefaultLabeler(); } if (customCommandHandler == null) { customCommandHandler = new CustomCommandHandler() { @Override public MessagePayload handle(CustomMessage request) { - throw new RuntimeException("Received custom request, but no custom command handler configured."); + throw new IllegalStateException( + "Received custom request, but no custom command handler configured."); } }; } diff --git a/core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java b/core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java index b2366e4..b61f747 100644 --- a/core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java +++ b/core/src/main/java/ch/dissem/bitmessage/DefaultMessageListener.java @@ -20,8 +20,9 @@ import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.payload.*; -import ch.dissem.bitmessage.entity.valueobject.Label; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.exception.DecryptionFailedException; +import ch.dissem.bitmessage.ports.Labeler; import ch.dissem.bitmessage.ports.NetworkHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,23 +31,34 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; -import static ch.dissem.bitmessage.entity.Plaintext.Status.*; -import static ch.dissem.bitmessage.utils.UnixTime.DAY; +import static ch.dissem.bitmessage.entity.Plaintext.Status.PUBKEY_REQUESTED; -class DefaultMessageListener implements NetworkHandler.MessageListener { +class DefaultMessageListener implements NetworkHandler.MessageListener, InternalContext.ContextHolder { private final static Logger LOG = LoggerFactory.getLogger(DefaultMessageListener.class); - private final InternalContext ctx; + private final Labeler labeler; private final BitmessageContext.Listener listener; + private InternalContext ctx; - public DefaultMessageListener(InternalContext context, BitmessageContext.Listener listener) { - this.ctx = context; + public DefaultMessageListener(Labeler labeler, BitmessageContext.Listener listener) { + this.labeler = labeler; this.listener = listener; } @Override + public void setContext(InternalContext context) { + this.ctx = context; + } + + @Override + @SuppressWarnings("ConstantConditions") public void receive(ObjectMessage object) throws IOException { ObjectPayload payload = object.getPayload(); - if (payload.getType() == null) return; + if (payload.getType() == null) { + if (payload instanceof GenericPayload) { + receive((GenericPayload) payload); + } + return; + } switch (payload.getType()) { case GET_PUBKEY: { @@ -65,12 +77,15 @@ class DefaultMessageListener implements NetworkHandler.MessageListener { receive(object, (Broadcast) payload); break; } + default: { + throw new IllegalArgumentException("Unknown payload type " + payload.getType()); + } } } protected void receive(ObjectMessage object, GetPubkey getPubkey) { BitmessageAddress identity = ctx.getAddressRepository().findIdentity(getPubkey.getRipeTag()); - if (identity != null && identity.getPrivateKey() != null) { + 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 ctx.sendPubkey(identity, object.getStream()); @@ -96,23 +111,16 @@ class DefaultMessageListener implements NetworkHandler.MessageListener { } } - private void updatePubkey(BitmessageAddress address, Pubkey pubkey){ + private void updatePubkey(BitmessageAddress address, Pubkey pubkey) { address.setPubkey(pubkey); LOG.info("Got pubkey for contact " + address); ctx.getAddressRepository().save(address); - List messages = ctx.getMessageRepository().findMessages(Plaintext.Status.PUBKEY_REQUESTED, address); + List<Plaintext> messages = ctx.getMessageRepository().findMessages(PUBKEY_REQUESTED, address); LOG.info("Sending " + messages.size() + " messages for contact " + address); for (Plaintext msg : messages) { - msg.setStatus(DOING_PROOF_OF_WORK); - ctx.getMessageRepository().save(msg); - ctx.send( - msg.getFrom(), - msg.getTo(), - new Msg(msg), - +2 * DAY - ); - msg.setStatus(SENT); + ctx.getLabeler().markAsSending(msg); ctx.getMessageRepository().save(msg); + ctx.send(msg); } } @@ -120,16 +128,12 @@ class DefaultMessageListener implements NetworkHandler.MessageListener { for (BitmessageAddress identity : ctx.getAddressRepository().getIdentities()) { try { msg.decrypt(identity.getPrivateKey().getPrivateEncryptionKey()); - msg.getPlaintext().setTo(identity); - if (!object.isSignatureValid(msg.getPlaintext().getFrom().getPubkey())) { + Plaintext plaintext = msg.getPlaintext(); + plaintext.setTo(identity); + if (!object.isSignatureValid(plaintext.getFrom().getPubkey())) { LOG.warn("Msg with IV " + object.getInventoryVector() + " was successfully decrypted, but signature check failed. Ignoring."); } else { - msg.getPlaintext().setStatus(RECEIVED); - msg.getPlaintext().addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.UNREAD)); - msg.getPlaintext().setInventoryVector(object.getInventoryVector()); - ctx.getMessageRepository().save(msg.getPlaintext()); - listener.receive(msg.getPlaintext()); - updatePubkey(msg.getPlaintext().getFrom(), msg.getPlaintext().getFrom().getPubkey()); + receive(object.getInventoryVector(), plaintext); } break; } catch (DecryptionFailedException ignore) { @@ -137,6 +141,16 @@ class DefaultMessageListener implements NetworkHandler.MessageListener { } } + protected void receive(GenericPayload ack) { + if (ack.getData().length == Msg.ACK_LENGTH) { + Plaintext msg = ctx.getMessageRepository().getMessageForAck(ack.getData()); + if (msg != null) { + ctx.getLabeler().markAsAcknowledged(msg); + ctx.getMessageRepository().save(msg); + } + } + } + protected void receive(ObjectMessage object, Broadcast broadcast) throws IOException { byte[] tag = broadcast instanceof V5Broadcast ? ((V5Broadcast) broadcast).getTag() : null; for (BitmessageAddress subscription : ctx.getAddressRepository().getSubscriptions(broadcast.getVersion())) { @@ -148,15 +162,26 @@ class DefaultMessageListener implements NetworkHandler.MessageListener { if (!object.isSignatureValid(broadcast.getPlaintext().getFrom().getPubkey())) { LOG.warn("Broadcast with IV " + object.getInventoryVector() + " was successfully decrypted, but signature check failed. Ignoring."); } else { - broadcast.getPlaintext().setStatus(RECEIVED); - broadcast.getPlaintext().addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.BROADCAST, Label.Type.UNREAD)); - broadcast.getPlaintext().setInventoryVector(object.getInventoryVector()); - ctx.getMessageRepository().save(broadcast.getPlaintext()); - listener.receive(broadcast.getPlaintext()); - updatePubkey(broadcast.getPlaintext().getFrom(), broadcast.getPlaintext().getFrom().getPubkey()); + receive(object.getInventoryVector(), broadcast.getPlaintext()); } } catch (DecryptionFailedException ignore) { } } } + + protected void receive(InventoryVector iv, Plaintext msg) { + msg.setInventoryVector(iv); + labeler.setLabels(msg); + ctx.getMessageRepository().save(msg); + listener.receive(msg); + updatePubkey(msg.getFrom(), msg.getFrom().getPubkey()); + + if (msg.getType() == Plaintext.Type.MSG && msg.getTo().has(Pubkey.Feature.DOES_ACK)) { + ObjectMessage ack = msg.getAckMessage(); + if (ack != null) { + ctx.getInventory().storeObject(ack); + ctx.getNetworkHandler().offer(ack.getInventoryVector()); + } + } + } } diff --git a/core/src/main/java/ch/dissem/bitmessage/InternalContext.java b/core/src/main/java/ch/dissem/bitmessage/InternalContext.java index bfd4f30..e01a90a 100644 --- a/core/src/main/java/ch/dissem/bitmessage/InternalContext.java +++ b/core/src/main/java/ch/dissem/bitmessage/InternalContext.java @@ -16,10 +16,9 @@ package ch.dissem.bitmessage; -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.Encrypted; -import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.*; import ch.dissem.bitmessage.entity.payload.*; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.ports.*; import ch.dissem.bitmessage.utils.Singleton; import ch.dissem.bitmessage.utils.TTL; @@ -29,6 +28,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Arrays; +import java.util.List; import java.util.TreeSet; /** @@ -42,6 +42,9 @@ import java.util.TreeSet; public class InternalContext { private final static Logger LOG = LoggerFactory.getLogger(InternalContext.class); + public final static long NETWORK_NONCE_TRIALS_PER_BYTE = 1000; + public final static long NETWORK_EXTRA_BYTES = 1000; + private final Cryptography cryptography; private final Inventory inventory; private final NodeRegistry nodeRegistry; @@ -50,15 +53,14 @@ public class InternalContext { private final MessageRepository messageRepository; private final ProofOfWorkRepository proofOfWorkRepository; private final ProofOfWorkEngine proofOfWorkEngine; - private final MessageCallback messageCallback; private final CustomCommandHandler customCommandHandler; private final ProofOfWorkService proofOfWorkService; + private final Labeler labeler; + private final NetworkHandler.MessageListener networkListener; private final TreeSet<Long> streams = new TreeSet<>(); private final int port; private final long clientNonce; - private final long networkNonceTrialsPerByte = 1000; - private final long networkExtraBytes = 1000; private long connectionTTL; private int connectionLimit; @@ -73,11 +75,12 @@ public class InternalContext { this.proofOfWorkService = new ProofOfWorkService(); this.proofOfWorkEngine = builder.proofOfWorkEngine; this.clientNonce = cryptography.randomNonce(); - this.messageCallback = builder.messageCallback; this.customCommandHandler = builder.customCommandHandler; this.port = builder.port; this.connectionLimit = builder.connectionLimit; this.connectionTTL = builder.connectionTTL; + this.labeler = builder.labeler; + this.networkListener = new DefaultMessageListener(labeler, builder.listener); Singleton.initialize(cryptography); @@ -93,8 +96,8 @@ public class InternalContext { } init(cryptography, inventory, nodeRegistry, networkHandler, addressRepository, messageRepository, - proofOfWorkRepository, proofOfWorkService, proofOfWorkEngine, - messageCallback, customCommandHandler); + proofOfWorkRepository, proofOfWorkService, proofOfWorkEngine, customCommandHandler, builder.labeler, + networkListener); for (BitmessageAddress identity : addressRepository.getIdentities()) { streams.add(identity.getStream()); } @@ -144,6 +147,14 @@ public class InternalContext { return proofOfWorkService; } + public Labeler getLabeler() { + return labeler; + } + + public NetworkHandler.MessageListener getNetworkListener() { + return networkListener; + } + public long[] getStreams() { long[] result = new long[streams.size()]; int i = 0; @@ -157,37 +168,38 @@ public class InternalContext { return port; } - public long getNetworkNonceTrialsPerByte() { - return networkNonceTrialsPerByte; - } - - public long getNetworkExtraBytes() { - return networkExtraBytes; + public void send(final Plaintext plaintext) { + if (plaintext.getAckMessage() != null) { + long expires = UnixTime.now(+plaintext.getTTL()); + LOG.info("Expires at " + expires); + proofOfWorkService.doProofOfWorkWithAck(plaintext, expires); + } else { + send(plaintext.getFrom(), plaintext.getTo(), new Msg(plaintext), plaintext.getTTL()); + } } public void send(final BitmessageAddress from, BitmessageAddress to, final ObjectPayload payload, final long timeToLive) { try { - if (to == null) to = from; + final BitmessageAddress recipient = (to != null ? to : from); long expires = UnixTime.now(+timeToLive); LOG.info("Expires at " + expires); final ObjectMessage object = new ObjectMessage.Builder() - .stream(to.getStream()) - .expiresTime(expires) - .payload(payload) - .build(); + .stream(recipient.getStream()) + .expiresTime(expires) + .payload(payload) + .build(); if (object.isSigned()) { object.sign(from.getPrivateKey()); } if (payload instanceof Broadcast) { ((Broadcast) payload).encrypt(); } else if (payload instanceof Encrypted) { - object.encrypt(to.getPubkey()); + object.encrypt(recipient.getPubkey()); } - messageCallback.proofOfWorkStarted(payload); proofOfWorkService.doProofOfWork(to, object); } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -196,17 +208,16 @@ public class InternalContext { long expires = UnixTime.now(TTL.pubkey()); LOG.info("Expires at " + expires); final ObjectMessage response = new ObjectMessage.Builder() - .stream(targetStream) - .expiresTime(expires) - .payload(identity.getPubkey()) - .build(); + .stream(targetStream) + .expiresTime(expires) + .payload(identity.getPubkey()) + .build(); response.sign(identity.getPrivateKey()); response.encrypt(cryptography.createPublicKey(identity.getPublicDecryptionKey())); - messageCallback.proofOfWorkStarted(identity.getPubkey()); // TODO: remember that the pubkey is just about to be sent, and on which stream! proofOfWorkService.doProofOfWork(response); } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -235,11 +246,10 @@ public class InternalContext { 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(); - messageCallback.proofOfWorkStarted(request.getPayload()); + .stream(contact.getStream()) + .expiresTime(expires) + .payload(new GetPubkey(contact)) + .build(); proofOfWorkService.doProofOfWork(request); } @@ -277,6 +287,14 @@ public class InternalContext { } } + public void resendUnacknowledged() { + List<Plaintext> messages = messageRepository.findMessagesToResend(); + for (Plaintext message : messages) { + send(message); + messageRepository.save(message); + } + } + public long getClientNonce() { return clientNonce; } diff --git a/core/src/main/java/ch/dissem/bitmessage/MessageCallback.java b/core/src/main/java/ch/dissem/bitmessage/MessageCallback.java deleted file mode 100644 index d09ff97..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/MessageCallback.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2015 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; - -import ch.dissem.bitmessage.entity.payload.ObjectPayload; -import ch.dissem.bitmessage.entity.valueobject.InventoryVector; - -/** - * Callback for message sending events, mostly so the user can be notified when POW is done. - */ -public interface MessageCallback { - /** - * Called before calculation of proof of work begins. - */ - void proofOfWorkStarted(ObjectPayload message); - - /** - * Called after calculation of proof of work finished. - */ - void proofOfWorkCompleted(ObjectPayload message); - - /** - * Called once the message is offered to the network. Please note that this doesn't mean the message was sent, - * if the client is not connected to the network it's just stored in the inventory. - * <p> - * Also, please note that this is where the original payload as well as the {@link InventoryVector} of the sent - * message is available. If the callback needs the IV for some reason, it should be retrieved here. (Plaintext - * and Broadcast messages will have their IV property set automatically though.) - * </p> - */ - void messageOffered(ObjectPayload message, InventoryVector iv); - - /** - * This isn't called yet, as ACK messages aren't being processed yet. Also, this is only relevant for Plaintext - * messages. - */ - void messageAcknowledged(InventoryVector iv); -} diff --git a/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java b/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java index ecd1099..7c72bea 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java +++ b/core/src/main/java/ch/dissem/bitmessage/ProofOfWorkService.java @@ -1,20 +1,24 @@ package ch.dissem.bitmessage; -import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.entity.ObjectMessage; -import ch.dissem.bitmessage.entity.Plaintext; -import ch.dissem.bitmessage.entity.PlaintextHolder; +import ch.dissem.bitmessage.entity.*; +import ch.dissem.bitmessage.entity.payload.Msg; import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.ports.Cryptography; import ch.dissem.bitmessage.ports.MessageRepository; import ch.dissem.bitmessage.ports.ProofOfWorkEngine; import ch.dissem.bitmessage.ports.ProofOfWorkRepository; -import ch.dissem.bitmessage.ports.Cryptography; +import ch.dissem.bitmessage.ports.ProofOfWorkRepository.Item; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.util.List; +import java.util.Timer; +import java.util.TimerTask; -import static ch.dissem.bitmessage.utils.Singleton.security; +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.utils.Singleton.cryptography; /** * @author Christian Basler @@ -27,15 +31,22 @@ public class ProofOfWorkService implements ProofOfWorkEngine.Callback, InternalC private ProofOfWorkRepository powRepo; private MessageRepository messageRepo; - public void doMissingProofOfWork() { - List<byte[]> items = powRepo.getItems(); + public void doMissingProofOfWork(long delayInMilliseconds) { + final List<byte[]> items = powRepo.getItems(); if (items.isEmpty()) return; - LOG.info("Doing POW for " + items.size() + " tasks."); - for (byte[] initialHash : items) { - ProofOfWorkRepository.Item item = powRepo.getItem(initialHash); - cryptography.doProofOfWork(item.object, item.nonceTrialsPerByte, item.extraBytes, this); - } + // Wait for 30 seconds, to let the application start up before putting heavy load on the CPU + new Timer().schedule(new TimerTask() { + @Override + public void run() { + LOG.info("Doing POW for " + items.size() + " tasks."); + for (byte[] initialHash : items) { + Item item = powRepo.getItem(initialHash); + cryptography.doProofOfWork(item.object, item.nonceTrialsPerByte, item.extraBytes, + ProofOfWorkService.this); + } + } + }, delayInMilliseconds); } public void doProofOfWork(ObjectMessage object) { @@ -45,8 +56,8 @@ public class ProofOfWorkService implements ProofOfWorkEngine.Callback, InternalC public void doProofOfWork(BitmessageAddress recipient, ObjectMessage object) { Pubkey pubkey = recipient == null ? null : recipient.getPubkey(); - long nonceTrialsPerByte = pubkey == null ? ctx.getNetworkNonceTrialsPerByte() : pubkey.getNonceTrialsPerByte(); - long extraBytes = pubkey == null ? ctx.getNetworkExtraBytes() : pubkey.getExtraBytes(); + long nonceTrialsPerByte = pubkey == null ? NETWORK_NONCE_TRIALS_PER_BYTE : pubkey.getNonceTrialsPerByte(); + long extraBytes = pubkey == null ? NETWORK_EXTRA_BYTES : pubkey.getExtraBytes(); powRepo.putObject(object, nonceTrialsPerByte, extraBytes); if (object.getPayload() instanceof PlaintextHolder) { @@ -57,26 +68,57 @@ public class ProofOfWorkService implements ProofOfWorkEngine.Callback, InternalC cryptography.doProofOfWork(object, nonceTrialsPerByte, extraBytes, this); } + public void doProofOfWorkWithAck(Plaintext plaintext, long expirationTime) { + final ObjectMessage ack = plaintext.getAckMessage(); + messageRepo.save(plaintext); + Item item = new Item(ack, NETWORK_NONCE_TRIALS_PER_BYTE, NETWORK_EXTRA_BYTES, + expirationTime, plaintext); + powRepo.putObject(item); + cryptography.doProofOfWork(ack, NETWORK_NONCE_TRIALS_PER_BYTE, NETWORK_EXTRA_BYTES, this); + } + @Override public void onNonceCalculated(byte[] initialHash, byte[] nonce) { - ObjectMessage object = powRepo.getItem(initialHash).object; - object.setNonce(nonce); -// messageCallback.proofOfWorkCompleted(payload); - Plaintext plaintext = messageRepo.getMessage(initialHash); - if (plaintext != null) { - plaintext.setInventoryVector(object.getInventoryVector()); - messageRepo.save(plaintext); + Item item = powRepo.getItem(initialHash); + if (item.message == null) { + ObjectMessage object = powRepo.getItem(initialHash).object; + object.setNonce(nonce); + Plaintext plaintext = messageRepo.getMessage(initialHash); + if (plaintext != null) { + plaintext.setInventoryVector(object.getInventoryVector()); + plaintext.updateNextTry(); + ctx.getLabeler().markAsSent(plaintext); + messageRepo.save(plaintext); + } + try { + ctx.getNetworkListener().receive(object); + } catch (IOException e) { + LOG.debug(e.getMessage(), e); + } + ctx.getInventory().storeObject(object); + ctx.getNetworkHandler().offer(object.getInventoryVector()); + } else { + item.message.getAckMessage().setNonce(nonce); + final ObjectMessage object = new ObjectMessage.Builder() + .stream(item.message.getStream()) + .expiresTime(item.expirationTime) + .payload(new Msg(item.message)) + .build(); + if (object.isSigned()) { + object.sign(item.message.getFrom().getPrivateKey()); + } + if (object.getPayload() instanceof Encrypted) { + object.encrypt(item.message.getTo().getPubkey()); + } + doProofOfWork(item.message.getTo(), object); } - ctx.getInventory().storeObject(object); - ctx.getProofOfWorkRepository().removeObject(initialHash); - ctx.getNetworkHandler().offer(object.getInventoryVector()); -// messageCallback.messageOffered(payload, object.getInventoryVector()); + powRepo.removeObject(initialHash); } @Override public void setContext(InternalContext ctx) { this.ctx = ctx; - this.cryptography = security(); + this.cryptography = cryptography(); this.powRepo = ctx.getProofOfWorkRepository(); this.messageRepo = ctx.getMessageRepository(); } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/Addr.java b/core/src/main/java/ch/dissem/bitmessage/entity/Addr.java index d00623e..73d9995 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Addr.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Addr.java @@ -21,6 +21,7 @@ import ch.dissem.bitmessage.utils.Encode; import java.io.IOException; import java.io.OutputStream; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -29,6 +30,8 @@ import java.util.List; * The 'addr' command holds a list of known active Bitmessage nodes. */ public class Addr implements MessagePayload { + private static final long serialVersionUID = -5117688017050138720L; + private final List<NetworkAddress> addresses; private Addr(Builder builder) { @@ -45,18 +48,23 @@ public class Addr implements MessagePayload { } @Override - public void write(OutputStream stream) throws IOException { - Encode.varInt(addresses.size(), stream); + public void write(OutputStream out) throws IOException { + Encode.varInt(addresses.size(), out); for (NetworkAddress address : addresses) { - address.write(stream); + address.write(out); + } + } + + @Override + public void write(ByteBuffer buffer) { + Encode.varInt(addresses.size(), buffer); + for (NetworkAddress address : addresses) { + address.write(buffer); } } public static final class Builder { - private List<NetworkAddress> addresses = new ArrayList<NetworkAddress>(); - - public Builder() { - } + private List<NetworkAddress> addresses = new ArrayList<>(); public Builder addresses(Collection<NetworkAddress> addresses){ this.addresses.addAll(addresses); diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java b/core/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java index 931776c..ce199c4 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/BitmessageAddress.java @@ -17,8 +17,10 @@ package ch.dissem.bitmessage.entity; import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.entity.payload.Pubkey.Feature; import ch.dissem.bitmessage.entity.payload.V4Pubkey; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.utils.AccessCounter; import ch.dissem.bitmessage.utils.Base58; import ch.dissem.bitmessage.utils.Bytes; @@ -28,18 +30,22 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.Serializable; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Objects; import static ch.dissem.bitmessage.utils.Decode.bytes; import static ch.dissem.bitmessage.utils.Decode.varInt; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; /** * A Bitmessage address. Can be a user's private address, an address string without public keys or a recipient's address * holding private keys. */ public class BitmessageAddress implements Serializable { + private static final long serialVersionUID = 2386328540805994064L; + private final long version; private final long stream; private final byte[] ripe; @@ -56,6 +62,7 @@ public class BitmessageAddress implements Serializable { private String alias; private boolean subscribed; + private boolean chan; BitmessageAddress(long version, long stream, byte[] ripe) { try { @@ -67,23 +74,23 @@ public class BitmessageAddress implements Serializable { Encode.varInt(version, os); Encode.varInt(stream, os); if (version < 4) { - byte[] checksum = security().sha512(os.toByteArray(), ripe); + byte[] checksum = cryptography().sha512(os.toByteArray(), ripe); this.tag = null; this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32); } else { // for tag and decryption key, the checksum has to be created with 0x00 padding - byte[] checksum = security().doubleSha512(os.toByteArray(), ripe); + byte[] checksum = cryptography().doubleSha512(os.toByteArray(), ripe); this.tag = Arrays.copyOfRange(checksum, 32, 64); this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32); } // but for the address and its checksum they need to be stripped int offset = Bytes.numberOfLeadingZeros(ripe); os.write(ripe, offset, ripe.length - offset); - byte[] checksum = security().doubleSha512(os.toByteArray()); + byte[] checksum = cryptography().doubleSha512(os.toByteArray()); os.write(checksum, 0, 4); this.address = "BM-" + Base58.encode(os.toByteArray()); } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -92,6 +99,38 @@ public class BitmessageAddress implements Serializable { this.pubkey = publicKey; } + public BitmessageAddress(String address, String passphrase) { + this(address); + this.privateKey = new PrivateKey(this, passphrase); + this.pubkey = this.privateKey.getPubkey(); + if (!Arrays.equals(ripe, privateKey.getPubkey().getRipe())) { + throw new IllegalArgumentException("Wrong address or passphrase"); + } + } + + public static BitmessageAddress chan(String address, String passphrase) { + BitmessageAddress result = new BitmessageAddress(address, passphrase); + result.chan = true; + return result; + } + + public static BitmessageAddress chan(long stream, String passphrase) { + PrivateKey privateKey = new PrivateKey(Pubkey.LATEST_VERSION, stream, passphrase); + BitmessageAddress result = new BitmessageAddress(privateKey); + result.chan = true; + return result; + } + + public static List<BitmessageAddress> deterministic(String passphrase, int numberOfAddresses, + long version, long stream, boolean shorter) { + List<BitmessageAddress> result = new ArrayList<>(numberOfAddresses); + List<PrivateKey> privateKeys = PrivateKey.deterministic(passphrase, numberOfAddresses, version, stream, shorter); + for (PrivateKey pk : privateKeys) { + result.add(new BitmessageAddress(pk)); + } + return result; + } + public BitmessageAddress(PrivateKey privateKey) { this(privateKey.getPubkey()); this.privateKey = privateKey; @@ -108,23 +147,23 @@ public class BitmessageAddress implements Serializable { this.ripe = Bytes.expand(bytes(in, bytes.length - counter.length() - 4), 20); // test checksum - byte[] checksum = security().doubleSha512(bytes, bytes.length - 4); + byte[] checksum = cryptography().doubleSha512(bytes, bytes.length - 4); byte[] expectedChecksum = bytes(in, 4); for (int i = 0; i < 4; i++) { if (expectedChecksum[i] != checksum[i]) throw new IllegalArgumentException("Checksum of address failed"); } if (version < 4) { - checksum = security().sha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe); + checksum = cryptography().sha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe); this.tag = null; this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32); } else { - checksum = security().doubleSha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe); + checksum = cryptography().doubleSha512(Arrays.copyOfRange(bytes, 0, counter.length()), ripe); this.tag = Arrays.copyOfRange(checksum, 32, 64); this.publicDecryptionKey = Arrays.copyOfRange(checksum, 0, 32); } } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -134,9 +173,9 @@ public class BitmessageAddress implements Serializable { Encode.varInt(version, out); Encode.varInt(stream, out); out.write(ripe); - return Arrays.copyOfRange(security().doubleSha512(out.toByteArray()), 32, 64); + return Arrays.copyOfRange(cryptography().doubleSha512(out.toByteArray()), 32, 64); } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -187,7 +226,7 @@ public class BitmessageAddress implements Serializable { @Override public String toString() { - return alias != null ? alias : address; + return alias == null ? address : alias; } public byte[] getRipe() { @@ -220,4 +259,19 @@ public class BitmessageAddress implements Serializable { public void setSubscribed(boolean subscribed) { this.subscribed = subscribed; } + + public boolean isChan() { + return chan; + } + + public void setChan(boolean chan) { + this.chan = chan; + } + + public boolean has(Feature feature) { + if (pubkey == null || feature == null) { + return false; + } + return feature.isActive(pubkey.getBehaviorBitfield()); + } } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/CustomMessage.java b/core/src/main/java/ch/dissem/bitmessage/entity/CustomMessage.java index 5702b6e..439f003 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/CustomMessage.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/CustomMessage.java @@ -16,10 +16,12 @@ package ch.dissem.bitmessage.entity; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.utils.AccessCounter; import ch.dissem.bitmessage.utils.Encode; import java.io.*; +import java.nio.ByteBuffer; import static ch.dissem.bitmessage.utils.Decode.bytes; import static ch.dissem.bitmessage.utils.Decode.varString; @@ -28,6 +30,8 @@ import static ch.dissem.bitmessage.utils.Decode.varString; * @author Christian Basler */ public class CustomMessage implements MessagePayload { + private static final long serialVersionUID = -8932056829480326011L; + public static final String COMMAND_ERROR = "ERROR"; private final String command; @@ -66,7 +70,7 @@ public class CustomMessage implements MessagePayload { write(out); return out.toByteArray(); } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } } @@ -77,7 +81,18 @@ public class CustomMessage implements MessagePayload { Encode.varString(command, out); out.write(data); } else { - throw new RuntimeException("Tried to write custom message without data. " + + throw new ApplicationException("Tried to write custom message without data. " + + "Programmer: did you forget to override #write()?"); + } + } + + @Override + public void write(ByteBuffer buffer) { + if (data != null) { + Encode.varString(command, buffer); + buffer.put(data); + } else { + throw new ApplicationException("Tried to write custom message without data. " + "Programmer: did you forget to override #write()?"); } } @@ -90,7 +105,7 @@ public class CustomMessage implements MessagePayload { try { return new CustomMessage(COMMAND_ERROR, message.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/GetData.java b/core/src/main/java/ch/dissem/bitmessage/entity/GetData.java index e272bbc..7d14fa0 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/GetData.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/GetData.java @@ -21,6 +21,7 @@ import ch.dissem.bitmessage.utils.Encode; import java.io.IOException; import java.io.OutputStream; +import java.nio.ByteBuffer; import java.util.LinkedList; import java.util.List; @@ -28,6 +29,10 @@ import java.util.List; * The 'getdata' command is used to request objects from a node. */ public class GetData implements MessagePayload { + private static final long serialVersionUID = 1433878785969631061L; + + public static final int MAX_INVENTORY_SIZE = 50_000; + List<InventoryVector> inventory; private GetData(Builder builder) { @@ -51,12 +56,17 @@ public class GetData implements MessagePayload { } } + @Override + public void write(ByteBuffer buffer) { + Encode.varInt(inventory.size(), buffer); + for (InventoryVector iv : inventory) { + iv.write(buffer); + } + } + public static final class Builder { private List<InventoryVector> inventory = new LinkedList<>(); - public Builder() { - } - public Builder addInventoryVector(InventoryVector inventoryVector) { this.inventory.add(inventoryVector); return this; diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/Inv.java b/core/src/main/java/ch/dissem/bitmessage/entity/Inv.java index 0135ec0..8d0f592 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Inv.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Inv.java @@ -21,6 +21,7 @@ import ch.dissem.bitmessage.utils.Encode; import java.io.IOException; import java.io.OutputStream; +import java.nio.ByteBuffer; import java.util.LinkedList; import java.util.List; @@ -28,6 +29,8 @@ import java.util.List; * The 'inv' command holds up to 50000 inventory vectors, i.e. hashes of inventory items. */ public class Inv implements MessagePayload { + private static final long serialVersionUID = 3662992522956947145L; + private List<InventoryVector> inventory; private Inv(Builder builder) { @@ -51,12 +54,17 @@ public class Inv implements MessagePayload { } } + @Override + public void write(ByteBuffer buffer) { + Encode.varInt(inventory.size(), buffer); + for (InventoryVector iv : inventory) { + iv.write(buffer); + } + } + public static final class Builder { private List<InventoryVector> inventory = new LinkedList<>(); - public Builder() { - } - public Builder addInventoryVector(InventoryVector inventoryVector) { this.inventory.add(inventoryVector); return this; diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java b/core/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java index 8790d3a..f27384e 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/NetworkMessage.java @@ -16,22 +16,25 @@ package ch.dissem.bitmessage.entity; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.utils.Encode; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.security.GeneralSecurityException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; /** * A network message is exchanged between two nodes. */ public class NetworkMessage implements Streamable { + private static final long serialVersionUID = 702708857104464809L; + /** * Magic value indicating message origin network, and used to seek to next message when stream state is unknown */ @@ -48,7 +51,7 @@ public class NetworkMessage implements Streamable { * First 4 bytes of sha512(payload) */ private byte[] getChecksum(byte[] bytes) throws NoSuchProviderException, NoSuchAlgorithmException { - byte[] d = security().sha512(bytes); + byte[] d = cryptography().sha512(bytes); return new byte[]{d[0], d[1], d[2], d[3]}; } @@ -71,9 +74,7 @@ public class NetworkMessage implements Streamable { out.write('\0'); } - ByteArrayOutputStream payloadStream = new ByteArrayOutputStream(); - payload.write(payloadStream); - byte[] payloadBytes = payloadStream.toByteArray(); + byte[] payloadBytes = Encode.bytes(payload); // Length of payload in number of bytes. Because of other restrictions, there is no reason why this length would // ever be larger than 1600003 bytes. Some clients include a sanity-check to avoid processing messages which are @@ -84,10 +85,67 @@ public class NetworkMessage implements Streamable { try { out.write(getChecksum(payloadBytes)); } catch (GeneralSecurityException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } // message payload out.write(payloadBytes); } + + /** + * A more efficient implementation of the write method, writing header data to the provided buffer and returning + * a new buffer containing the payload. + * + * @param headerBuffer where the header data is written to (24 bytes) + * @return a buffer containing the payload, ready to be read. + */ + public ByteBuffer writeHeaderAndGetPayloadBuffer(ByteBuffer headerBuffer) { + return ByteBuffer.wrap(writeHeader(headerBuffer)); + } + + /** + * For improved memory efficiency, you should use {@link #writeHeaderAndGetPayloadBuffer(ByteBuffer)} + * and write the header buffer as well as the returned payload buffer into the channel. + * + * @param buffer where everything gets written to. Needs to be large enough for the whole message + * to be written. + */ + @Override + public void write(ByteBuffer buffer) { + byte[] payloadBytes = writeHeader(buffer); + buffer.put(payloadBytes); + } + + private byte[] writeHeader(ByteBuffer out) { + // magic + Encode.int32(MAGIC, out); + + // ASCII string identifying the packet content, NULL padded (non-NULL padding results in packet rejected) + String command = payload.getCommand().name().toLowerCase(); + try { + out.put(command.getBytes("ASCII")); + } catch (UnsupportedEncodingException e) { + throw new ApplicationException(e); + } + for (int i = command.length(); i < 12; i++) { + out.put((byte) 0); + } + + byte[] payloadBytes = Encode.bytes(payload); + + // Length of payload in number of bytes. Because of other restrictions, there is no reason why this length would + // ever be larger than 1600003 bytes. Some clients include a sanity-check to avoid processing messages which are + // larger than this. + Encode.int32(payloadBytes.length, out); + + // checksum + try { + out.put(getChecksum(payloadBytes)); + } catch (GeneralSecurityException e) { + throw new ApplicationException(e); + } + + // message payload + return payloadBytes; + } } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java b/core/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java index 99b3aec..fee761c 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/ObjectMessage.java @@ -21,6 +21,7 @@ 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.entity.valueobject.PrivateKey; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.utils.Bytes; import ch.dissem.bitmessage.utils.Encode; @@ -28,13 +29,18 @@ import ch.dissem.bitmessage.utils.Encode; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Objects; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; /** * The 'object' command sends an object that is shared throughout the network. */ public class ObjectMessage implements MessagePayload { + private static final long serialVersionUID = 2495752480120659139L; + private byte[] nonce; private long expiresTime; private long objectType; @@ -52,7 +58,7 @@ public class ObjectMessage implements MessagePayload { expiresTime = builder.expiresTime; objectType = builder.objectType; version = builder.payload.getVersion(); - stream = builder.streamNumber; + stream = builder.streamNumber > 0 ? builder.streamNumber : builder.payload.getStream(); payload = builder.payload; } @@ -91,7 +97,7 @@ public class ObjectMessage implements MessagePayload { public InventoryVector getInventoryVector() { return new InventoryVector( - Bytes.truncate(security().doubleSha512(nonce, getPayloadBytesWithoutNonce()), 32) + Bytes.truncate(cryptography().doubleSha512(nonce, getPayloadBytesWithoutNonce()), 32) ); } @@ -110,13 +116,13 @@ public class ObjectMessage implements MessagePayload { payload.writeBytesToSign(out); return out.toByteArray(); } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } public void sign(PrivateKey key) { if (payload.isSigned()) { - payload.setSignature(security().getSignature(getBytesToSign(), key)); + payload.setSignature(cryptography().getSignature(getBytesToSign(), key)); } } @@ -144,25 +150,35 @@ public class ObjectMessage implements MessagePayload { ((Encrypted) payload).encrypt(publicKey.getEncryptionKey()); } } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } public boolean isSignatureValid(Pubkey pubkey) throws IOException { if (isEncrypted()) throw new IllegalStateException("Payload must be decrypted first"); - return security().isSignatureValid(getBytesToSign(), payload.getSignature(), pubkey); + return cryptography().isSignatureValid(getBytesToSign(), payload.getSignature(), pubkey); } @Override public void write(OutputStream out) throws IOException { - if (nonce != null) { - out.write(nonce); - } else { + if (nonce == null) { out.write(new byte[8]); + } else { + out.write(nonce); } out.write(getPayloadBytesWithoutNonce()); } + @Override + public void write(ByteBuffer buffer) { + if (nonce == null) { + buffer.put(new byte[8]); + } else { + buffer.put(nonce); + } + buffer.put(getPayloadBytesWithoutNonce()); + } + private void writeHeaderWithoutNonce(OutputStream out) throws IOException { Encode.int64(expiresTime, out); Encode.int32(objectType, out); @@ -180,7 +196,7 @@ public class ObjectMessage implements MessagePayload { } return payloadBytes; } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -191,9 +207,6 @@ public class ObjectMessage implements MessagePayload { private long streamNumber; private ObjectPayload payload; - public Builder() { - } - public Builder nonce(byte[] nonce) { this.nonce = nonce; return this; @@ -230,4 +243,29 @@ public class ObjectMessage implements MessagePayload { return new ObjectMessage(this); } } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ObjectMessage that = (ObjectMessage) o; + + return expiresTime == that.expiresTime && + objectType == that.objectType && + version == that.version && + stream == that.stream && + Objects.equals(payload, that.payload); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(nonce); + result = 31 * result + (int) (expiresTime ^ (expiresTime >>> 32)); + result = 31 * result + (int) (objectType ^ (objectType >>> 32)); + result = 31 * result + (int) (version ^ (version >>> 32)); + result = 31 * result + (int) (stream ^ (stream >>> 32)); + result = 31 * result + (payload != null ? payload.hashCode() : 0); + return result; + } } 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 fbd5d48..242eb95 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Plaintext.java @@ -16,25 +16,33 @@ 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.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.Decode; -import ch.dissem.bitmessage.utils.Encode; -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.utils.Singleton.cryptography; /** * The unencrypted message to be sent by 'msg' or 'broadcast'. */ public class Plaintext implements Streamable { + private static final long serialVersionUID = -5325729856394951079L; + private final Type type; private final BitmessageAddress from; private final long encoding; private final byte[] message; - private final byte[] ack; + private final byte[] ackData; + private ObjectMessage ackMessage; private Object id; private InventoryVector inventoryVector; private BitmessageAddress to; @@ -46,6 +54,10 @@ public class Plaintext implements Streamable { private Set<Label> labels; private byte[] initialHash; + private long ttl; + private int retries; + private Long nextTry; + private Plaintext(Builder builder) { id = builder.id; inventoryVector = builder.inventoryVector; @@ -54,12 +66,21 @@ public class Plaintext implements Streamable { to = builder.to; encoding = builder.encoding; message = builder.message; - ack = builder.ack; + ackData = builder.ackData; + if (builder.ackMessage != null && builder.ackMessage.length > 0) { + ackMessage = Factory.getObjectMessage( + 3, + new ByteArrayInputStream(builder.ackMessage), + builder.ackMessage.length); + } signature = builder.signature; status = builder.status; sent = builder.sent; received = builder.received; labels = builder.labels; + ttl = builder.ttl; + retries = builder.retries; + nextTry = builder.nextTry; } public static Plaintext read(Type type, InputStream in) throws IOException { @@ -82,7 +103,7 @@ public class Plaintext implements Streamable { .destinationRipe(type == Type.MSG ? Decode.bytes(in, 20) : null) .encoding(Decode.varInt(in)) .message(Decode.varBytes(in)) - .ack(type == Type.MSG ? Decode.varBytes(in) : null); + .ackMessage(type == Type.MSG ? Decode.varBytes(in) : null); } public InventoryVector getInventoryVector() { @@ -111,9 +132,9 @@ public class Plaintext implements Streamable { public void setTo(BitmessageAddress to) { if (this.to.getVersion() != 0) - throw new RuntimeException("Correct address already set"); + throw new IllegalStateException("Correct address already set"); if (!Arrays.equals(this.to.getRipe(), to.getRipe())) { - throw new RuntimeException("RIPEs don't match"); + throw new IllegalArgumentException("RIPEs don't match"); } this.to = to; } @@ -160,8 +181,13 @@ public class Plaintext implements Streamable { Encode.varInt(message.length, out); out.write(message); if (type == Type.MSG) { - Encode.varInt(ack.length, out); - out.write(ack); + if (to.has(Feature.DOES_ACK) && getAckMessage() != null) { + ByteArrayOutputStream ack = new ByteArrayOutputStream(); + getAckMessage().write(ack); + Encode.varBytes(ack.toByteArray(), out); + } else { + Encode.varInt(0, out); + } } if (includeSignature) { if (signature == null) { @@ -172,12 +198,49 @@ 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 (type == Type.MSG) { + buffer.put(to.getRipe()); + } + Encode.varInt(encoding, buffer); + Encode.varInt(message.length, buffer); + buffer.put(message); + if (type == Type.MSG) { + if (to.has(Feature.DOES_ACK) && getAckMessage() != null) { + Encode.varBytes(Encode.bytes(getAckMessage()), buffer); + } else { + Encode.varInt(0, buffer); + } + } + if (includeSignature) { + if (signature == null) { + Encode.varInt(0, buffer); + } else { + Encode.varInt(signature.length, buffer); + buffer.put(signature); + } + } + } @Override public void write(OutputStream out) throws IOException { write(out, true); } + @Override + public void write(ByteBuffer buffer) { + write(buffer, true); + } + public Object getId() { return id; } @@ -203,6 +266,30 @@ public class Plaintext implements Streamable { this.status = status; } + public long getTTL() { + return ttl; + } + + public int getRetries() { + return retries; + } + + public Long getNextTry() { + return nextTry; + } + + public void updateNextTry() { + if (nextTry == null) { + if (sent != null && to.has(Feature.DOES_ACK)) { + nextTry = UnixTime.now(+ttl); + retries++; + } + } else { + nextTry = nextTry + (1 << retries) * ttl; + retries++; + } + } + public String getSubject() { Scanner s = new Scanner(new ByteArrayInputStream(message), "UTF-8"); String firstLine = s.nextLine(); @@ -223,7 +310,7 @@ public class Plaintext implements Streamable { } return text; } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -235,7 +322,7 @@ public class Plaintext implements Streamable { return Objects.equals(encoding, plaintext.encoding) && Objects.equals(from, plaintext.from) && Arrays.equals(message, plaintext.message) && - Arrays.equals(ack, plaintext.ack) && + Objects.equals(getAckMessage(), plaintext.getAckMessage()) && Arrays.equals(to.getRipe(), plaintext.to.getRipe()) && Arrays.equals(signature, plaintext.signature) && Objects.equals(status, plaintext.status) && @@ -246,7 +333,7 @@ public class Plaintext implements Streamable { @Override public int hashCode() { - return Objects.hash(from, encoding, message, ack, to, signature, status, sent, received, labels); + return Objects.hash(from, encoding, message, ackData, to, signature, status, sent, received, labels); } public void addLabels(Label... labels) { @@ -257,10 +344,33 @@ public class Plaintext implements Streamable { public void addLabels(Collection<Label> labels) { if (labels != null) { - this.labels.addAll(labels); + for (Label label : labels) { + this.labels.add(label); + } } } + public void removeLabel(Label.Type type) { + Iterator<Label> iterator = labels.iterator(); + while (iterator.hasNext()) { + Label label = iterator.next(); + if (label.getType() == type) { + iterator.remove(); + } + } + } + + public byte[] getAckData() { + return ackData; + } + + public ObjectMessage getAckMessage() { + if (ackMessage == null) { + ackMessage = Factory.createAck(this); + } + return ackMessage; + } + public void setInitialHash(byte[] initialHash) { this.initialHash = initialHash; } @@ -313,12 +423,16 @@ public class Plaintext implements Streamable { private byte[] destinationRipe; private long encoding; private byte[] message = new byte[0]; - private byte[] ack = new byte[0]; + private byte[] ackData; + private byte[] ackMessage; private byte[] signature; private long sent; private long received; private Status status; private Set<Label> labels = new HashSet<>(); + private long ttl; + private int retries; + private Long nextTry; public Builder(Type type) { this.type = type; @@ -402,7 +516,7 @@ public class Plaintext implements Streamable { this.encoding = Encoding.SIMPLE.getCode(); this.message = ("Subject:" + subject + '\n' + "Body:" + message).getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } return this; } @@ -412,9 +526,16 @@ public class Plaintext implements Streamable { return this; } - public Builder ack(byte[] ack) { - if (type != Type.MSG && ack != null) throw new IllegalArgumentException("ack only allowed for msg"); - this.ack = ack; + public Builder ackMessage(byte[] ack) { + if (type != Type.MSG && ack != null) throw new IllegalArgumentException("ackMessage only allowed for msg"); + this.ackMessage = ack; + return this; + } + + public Builder ackData(byte[] ackData) { + if (type != Type.MSG && ackData != null) + throw new IllegalArgumentException("ackMessage only allowed for msg"); + this.ackData = ackData; return this; } @@ -443,6 +564,21 @@ public class Plaintext implements Streamable { return this; } + public Builder ttl(long ttl) { + this.ttl = ttl; + return this; + } + + public Builder retries(int retries) { + this.retries = retries; + return this; + } + + public Builder nextTry(Long nextTry) { + this.nextTry = nextTry; + return this; + } + public Plaintext build() { if (from == null) { from = new BitmessageAddress(Factory.createPubkey( @@ -455,9 +591,15 @@ public class Plaintext implements Streamable { behaviorBitfield )); } - if (to == null && type != Type.BROADCAST) { + if (to == null && type != Type.BROADCAST && destinationRipe != null) { to = new BitmessageAddress(0, 0, destinationRipe); } + if (type == Type.MSG && ackMessage == null && ackData == null) { + ackData = cryptography().randomBytes(Msg.ACK_LENGTH); + } + if (ttl <= 0) { + ttl = TTL.msg(); + } return new Plaintext(this); } } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/Streamable.java b/core/src/main/java/ch/dissem/bitmessage/entity/Streamable.java index cc12050..e75a926 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Streamable.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Streamable.java @@ -19,10 +19,13 @@ package ch.dissem.bitmessage.entity; import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; +import java.nio.ByteBuffer; /** * An object that can be written to an {@link OutputStream} */ public interface Streamable extends Serializable { void write(OutputStream stream) throws IOException; + + void write(ByteBuffer buffer); } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/VerAck.java b/core/src/main/java/ch/dissem/bitmessage/entity/VerAck.java index 1aad501..3d30f32 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/VerAck.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/VerAck.java @@ -18,11 +18,14 @@ package ch.dissem.bitmessage.entity; import java.io.IOException; import java.io.OutputStream; +import java.nio.ByteBuffer; /** * The 'verack' command answers a 'version' command, accepting the other node's version. */ public class VerAck implements MessagePayload { + private static final long serialVersionUID = -4302074845199181687L; + @Override public Command getCommand() { return Command.VERACK; @@ -32,4 +35,9 @@ public class VerAck implements MessagePayload { public void write(OutputStream stream) throws IOException { // 'verack' doesn't have any payload, so there is nothing to write } + + @Override + public void write(ByteBuffer buffer) { + // 'verack' doesn't have any payload, so there is nothing to write + } } 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 70c2ad2..4d0fd05 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/Version.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/Version.java @@ -23,12 +23,14 @@ import ch.dissem.bitmessage.utils.UnixTime; import java.io.IOException; import java.io.OutputStream; -import java.util.Random; +import java.nio.ByteBuffer; /** * The 'version' command advertises this node's latest supported protocol version upon initiation. */ public class Version implements MessagePayload { + private static final long serialVersionUID = 7219240857343176567L; + /** * Identifies protocol version being used by the node. Should equal 3. Nodes should disconnect if the remote node's * version is lower but continue with the connection if it is higher. @@ -132,6 +134,18 @@ public class Version implements MessagePayload { Encode.varIntList(streams, stream); } + @Override + public void write(ByteBuffer buffer) { + Encode.int32(version, buffer); + Encode.int64(services, buffer); + Encode.int64(timestamp, buffer); + addrRecv.write(buffer, true); + addrFrom.write(buffer, true); + Encode.int64(nonce, buffer); + Encode.varString(userAgent, buffer); + Encode.varIntList(streams, buffer); + } + public static final class Builder { private int version; @@ -143,16 +157,13 @@ public class Version implements MessagePayload { private String userAgent; private long[] streamNumbers; - public Builder() { - } - - public Builder defaults() { + public Builder defaults(long clientNonce) { version = BitmessageContext.CURRENT_VERSION; services = 1; timestamp = UnixTime.now(); - nonce = new Random().nextInt(); userAgent = "/Jabit:0.0.1/"; streamNumbers = new long[]{1}; + nonce = clientNonce; return this; } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/Broadcast.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/Broadcast.java index 47bf539..a583330 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/Broadcast.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/Broadcast.java @@ -23,15 +23,18 @@ import ch.dissem.bitmessage.entity.PlaintextHolder; import ch.dissem.bitmessage.exception.DecryptionFailedException; import java.io.IOException; +import java.util.Objects; import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; /** * Users who are subscribed to the sending address will see the message appear in their inbox. * Broadcasts are version 4 or 5. */ public abstract class Broadcast extends ObjectPayload implements Encrypted, PlaintextHolder { + private static final long serialVersionUID = 4064521827582239069L; + protected final long stream; protected CryptoBox encrypted; protected Plaintext plaintext; @@ -78,7 +81,7 @@ public abstract class Broadcast extends ObjectPayload implements Encrypted, Plai } public void encrypt() throws IOException { - encrypt(security().createPublicKey(plaintext.getFrom().getPublicDecryptionKey())); + encrypt(cryptography().createPublicKey(plaintext.getFrom().getPublicDecryptionKey())); } @Override @@ -94,4 +97,18 @@ public abstract class Broadcast extends ObjectPayload implements Encrypted, Plai public boolean isDecrypted() { return plaintext != null; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Broadcast broadcast = (Broadcast) o; + return stream == broadcast.stream && + (Objects.equals(encrypted, broadcast.encrypted) || Objects.equals(plaintext, broadcast.plaintext)); + } + + @Override + public int hashCode() { + return Objects.hash(stream); + } } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/CryptoBox.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/CryptoBox.java index fe45ac5..f7f3c15 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/CryptoBox.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/CryptoBox.java @@ -17,19 +17,22 @@ package ch.dissem.bitmessage.entity.payload; import ch.dissem.bitmessage.entity.Streamable; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.utils.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; +import java.nio.ByteBuffer; import java.util.Arrays; import static ch.dissem.bitmessage.entity.valueobject.PrivateKey.PRIVATE_KEY_SIZE; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; public class CryptoBox implements Streamable { + private static final long serialVersionUID = 7217659539975573852L; private static final Logger LOG = LoggerFactory.getLogger(CryptoBox.class); private final byte[] initializationVector; @@ -38,8 +41,6 @@ public class CryptoBox implements Streamable { private final byte[] mac; private byte[] encrypted; - private long addressVersion; - public CryptoBox(Streamable data, byte[] K) throws IOException { this(Encode.bytes(data), K); @@ -50,22 +51,22 @@ public class CryptoBox implements Streamable { // 1. The destination public key is called K. // 2. Generate 16 random bytes using a secure random number generator. Call them IV. - initializationVector = security().randomBytes(16); + initializationVector = cryptography().randomBytes(16); // 3. Generate a new random EC key pair with private key called r and public key called R. - byte[] r = security().randomBytes(PRIVATE_KEY_SIZE); - R = security().createPublicKey(r); + byte[] r = cryptography().randomBytes(PRIVATE_KEY_SIZE); + R = cryptography().createPublicKey(r); // 4. Do an EC point multiply with public key K and private key r. This gives you public key P. - byte[] P = security().multiply(K, r); + byte[] P = cryptography().multiply(K, r); byte[] X = Points.getX(P); // 5. Use the X component of public key P and calculate the SHA512 hash H. - byte[] H = security().sha512(X); + byte[] H = cryptography().sha512(X); // 6. The first 32 bytes of H are called key_e and the last 32 bytes are called key_m. byte[] key_e = Arrays.copyOfRange(H, 0, 32); byte[] key_m = Arrays.copyOfRange(H, 32, 64); // 7. Pad the input text to a multiple of 16 bytes, in accordance to PKCS7. // 8. Encrypt the data with AES-256-CBC, using IV as initialization vector, key_e as encryption key and the padded input text as payload. Call the output cipher text. - encrypted = security().crypt(true, data, key_e, initializationVector); + encrypted = cryptography().crypt(true, data, key_e, initializationVector); // 9. Calculate a 32 byte MAC with HMACSHA256, using key_m as salt and IV + R + cipher text as data. Call the output MAC. mac = calculateMac(key_m); @@ -75,7 +76,7 @@ public class CryptoBox implements Streamable { private CryptoBox(Builder builder) { initializationVector = builder.initializationVector; curveType = builder.curveType; - R = security().createPoint(builder.xComponent, builder.yComponent); + R = cryptography().createPoint(builder.xComponent, builder.yComponent); encrypted = builder.encrypted; mac = builder.mac; } @@ -101,9 +102,9 @@ public class CryptoBox implements Streamable { public InputStream decrypt(byte[] k) throws DecryptionFailedException { // 1. The private key used to decrypt is called k. // 2. Do an EC point multiply with private key k and public key R. This gives you public key P. - byte[] P = security().multiply(R, k); + byte[] P = cryptography().multiply(R, k); // 3. Use the X component of public key P and calculate the SHA512 hash H. - byte[] H = security().sha512(Arrays.copyOfRange(P, 1, 33)); + byte[] H = cryptography().sha512(Arrays.copyOfRange(P, 1, 33)); // 4. The first 32 bytes of H are called key_e and the last 32 bytes are called key_m. byte[] key_e = Arrays.copyOfRange(H, 0, 32); byte[] key_m = Arrays.copyOfRange(H, 32, 64); @@ -116,16 +117,16 @@ public class CryptoBox implements Streamable { // 7. Decrypt the cipher text with AES-256-CBC, using IV as initialization vector, key_e as decryption key // and the cipher text as payload. The output is the padded input text. - return new ByteArrayInputStream(security().crypt(false, encrypted, key_e, initializationVector)); + return new ByteArrayInputStream(cryptography().crypt(false, encrypted, key_e, initializationVector)); } private byte[] calculateMac(byte[] key_m) { try { ByteArrayOutputStream macData = new ByteArrayOutputStream(); writeWithoutMAC(macData); - return security().mac(key_m, macData.toByteArray()); + return cryptography().mac(key_m, macData.toByteArray()); } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -144,12 +145,29 @@ public class CryptoBox implements Streamable { out.write(x, offset, length); } + private void writeCoordinateComponent(ByteBuffer buffer, byte[] x) { + int offset = Bytes.numberOfLeadingZeros(x); + int length = x.length - offset; + Encode.int16(length, buffer); + buffer.put(x, offset, length); + } + @Override public void write(OutputStream stream) throws IOException { writeWithoutMAC(stream); stream.write(mac); } + @Override + public void write(ByteBuffer buffer) { + buffer.put(initializationVector); + Encode.int16(curveType, buffer); + writeCoordinateComponent(buffer, Points.getX(R)); + writeCoordinateComponent(buffer, Points.getY(R)); + buffer.put(encrypted); + buffer.put(mac); + } + public static final class Builder { private byte[] initializationVector; private int curveType; diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java index 7051814..9312160 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/GenericPayload.java @@ -21,6 +21,7 @@ import ch.dissem.bitmessage.utils.Decode; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.ByteBuffer; import java.util.Arrays; /** @@ -28,6 +29,8 @@ import java.util.Arrays; * have to know what it is. */ public class GenericPayload extends ObjectPayload { + private static final long serialVersionUID = -912314085064185940L; + private long stream; private byte[] data; @@ -37,7 +40,7 @@ public class GenericPayload extends ObjectPayload { this.data = data; } - public static GenericPayload read(long version, InputStream is, long stream, int length) throws IOException { + public static GenericPayload read(long version, long stream, InputStream is, int length) throws IOException { return new GenericPayload(version, stream, Decode.bytes(is, length)); } @@ -51,11 +54,20 @@ public class GenericPayload extends ObjectPayload { return stream; } + public byte[] getData() { + return data; + } + @Override public void write(OutputStream stream) throws IOException { stream.write(data); } + @Override + public void write(ByteBuffer buffer) { + buffer.put(data); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java index e31dbe3..d889489 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/GetPubkey.java @@ -22,11 +22,14 @@ import ch.dissem.bitmessage.utils.Decode; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.ByteBuffer; /** * Request for a public key. */ public class GetPubkey extends ObjectPayload { + private static final long serialVersionUID = -3634516646972610180L; + private long stream; private byte[] ripeTag; @@ -71,4 +74,9 @@ public class GetPubkey extends ObjectPayload { public void write(OutputStream stream) throws IOException { stream.write(ripeTag); } + + @Override + public void write(ByteBuffer buffer) { + buffer.put(ripeTag); + } } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/Msg.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/Msg.java index 52c36e7..dc36bb1 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/Msg.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/Msg.java @@ -24,6 +24,8 @@ import ch.dissem.bitmessage.exception.DecryptionFailedException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Objects; import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; @@ -31,6 +33,9 @@ import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; * Used for person-to-person messages. */ public class Msg extends ObjectPayload implements Encrypted, PlaintextHolder { + private static final long serialVersionUID = 4327495048296365733L; + public static final int ACK_LENGTH = 32; + private long stream; private CryptoBox encrypted; private Plaintext plaintext; @@ -106,4 +111,25 @@ public class Msg extends ObjectPayload implements Encrypted, PlaintextHolder { if (encrypted == null) throw new IllegalStateException("Msg must be signed and encrypted before writing it."); encrypted.write(out); } + + @Override + public void write(ByteBuffer buffer) { + if (encrypted == null) throw new IllegalStateException("Msg must be signed and encrypted before writing it."); + encrypted.write(buffer); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + Msg msg = (Msg) o; + return stream == msg.stream && + (Objects.equals(encrypted, msg.encrypted) || Objects.equals(plaintext, msg.plaintext)); + } + + @Override + public int hashCode() { + return (int) stream; + } } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectPayload.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectPayload.java index 0ca45cd..33da28d 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectPayload.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/ObjectPayload.java @@ -21,12 +21,13 @@ import ch.dissem.bitmessage.entity.Streamable; import java.io.IOException; import java.io.OutputStream; -import java.io.Serializable; /** * The payload of an 'object' command. This is shared by the network. */ public abstract class ObjectPayload implements Streamable { + private static final long serialVersionUID = -5034977402902364482L; + private final long version; protected ObjectPayload(long version) { diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java index 1243b32..27d2da9 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/Pubkey.java @@ -18,14 +18,17 @@ package ch.dissem.bitmessage.entity.payload; import java.io.IOException; import java.io.OutputStream; +import java.nio.ByteBuffer; import java.util.ArrayList; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; /** * Public keys for signing and encryption, the answer to a 'getpubkey' request. */ public abstract class Pubkey extends ObjectPayload { + private static final long serialVersionUID = -6634533361454999619L; + public final static long LATEST_VERSION = 4; protected Pubkey(long version) { @@ -33,7 +36,7 @@ public abstract class Pubkey extends ObjectPayload { } public static byte[] getRipe(byte[] publicSigningKey, byte[] publicEncryptionKey) { - return security().ripemd160(security().sha512(publicSigningKey, publicEncryptionKey)); + return cryptography().ripemd160(cryptography().sha512(publicSigningKey, publicEncryptionKey)); } public abstract byte[] getSigningKey(); @@ -43,7 +46,7 @@ public abstract class Pubkey extends ObjectPayload { public abstract int getBehaviorBitfield(); public byte[] getRipe() { - return security().ripemd160(security().sha512(getSigningKey(), getEncryptionKey())); + return cryptography().ripemd160(cryptography().sha512(getSigningKey(), getEncryptionKey())); } public long getNonceTrialsPerByte() { @@ -58,6 +61,10 @@ public abstract class Pubkey extends ObjectPayload { write(out); } + public void writeUnencrypted(ByteBuffer buffer){ + write(buffer); + } + protected byte[] add0x04(byte[] key) { if (key.length == 65) return key; byte[] result = new byte[65]; @@ -74,16 +81,19 @@ public abstract class Pubkey extends ObjectPayload { * Receiving node expects that the RIPE hash encoded in their address preceedes the encrypted message data of msg * messages bound for them. */ - INCLUDE_DESTINATION(1 << 30), + INCLUDE_DESTINATION(30), /** * If true, the receiving node does send acknowledgements (rather than dropping them). */ - DOES_ACK(1 << 31); + DOES_ACK(31); private int bit; - Feature(int bit) { - this.bit = bit; + Feature(int bitNumber) { + // The Bitmessage Protocol Specification starts counting at the most significant bit, + // thus the slightly awkward calculation. + // https://bitmessage.org/wiki/Protocol_specification#Pubkey_bitfield_features + this.bit = 1 << (31 - bitNumber); } public static int bitfield(Feature... features) { @@ -103,5 +113,9 @@ public abstract class Pubkey extends ObjectPayload { } return features.toArray(new Feature[features.size()]); } + + public boolean isActive(int bitfield) { + return (bitfield & bit) != 0; + } } } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java index da16929..d2901c1 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V2Pubkey.java @@ -22,11 +22,14 @@ import ch.dissem.bitmessage.utils.Encode; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.ByteBuffer; /** * A version 2 public key. */ public class V2Pubkey extends Pubkey { + private static final long serialVersionUID = -257598690676510460L; + protected long stream; protected int behaviorBitfield; protected byte[] publicSigningKey; // 64 Bytes @@ -84,10 +87,17 @@ public class V2Pubkey extends Pubkey { } @Override - public void write(OutputStream os) throws IOException { - Encode.int32(behaviorBitfield, os); - os.write(publicSigningKey, 1, 64); - os.write(publicEncryptionKey, 1, 64); + public void write(OutputStream out) throws IOException { + Encode.int32(behaviorBitfield, out); + out.write(publicSigningKey, 1, 64); + out.write(publicEncryptionKey, 1, 64); + } + + @Override + public void write(ByteBuffer buffer) { + Encode.int32(behaviorBitfield, buffer); + buffer.put(publicSigningKey, 1, 64); + buffer.put(publicEncryptionKey, 1, 64); } public static class Builder { @@ -96,9 +106,6 @@ public class V2Pubkey extends Pubkey { private byte[] publicSigningKey; private byte[] publicEncryptionKey; - public Builder() { - } - public Builder stream(long streamNumber) { this.streamNumber = streamNumber; return this; diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java index bf91afe..72358d0 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V3Pubkey.java @@ -29,6 +29,8 @@ import java.util.Objects; * A version 3 public key. */ public class V3Pubkey extends V2Pubkey { + private static final long serialVersionUID = 6958853116648528319L; + long nonceTrialsPerByte; long extraBytes; byte[] signature; @@ -123,9 +125,6 @@ public class V3Pubkey extends V2Pubkey { private long extraBytes; private byte[] signature = new byte[0]; - public Builder() { - } - public Builder stream(long streamNumber) { this.streamNumber = streamNumber; return this; diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Broadcast.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Broadcast.java index 39127a8..323da33 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Broadcast.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Broadcast.java @@ -22,12 +22,15 @@ import ch.dissem.bitmessage.entity.Plaintext; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.ByteBuffer; /** * Users who are subscribed to the sending address will see the message appear in their inbox. * Broadcasts are version 4 or 5. */ public class V4Broadcast extends Broadcast { + private static final long serialVersionUID = 195663108282762711L; + protected V4Broadcast(long version, long stream, CryptoBox encrypted, Plaintext plaintext) { super(version, stream, encrypted, plaintext); } @@ -56,4 +59,9 @@ public class V4Broadcast extends Broadcast { public void write(OutputStream out) throws IOException { encrypted.write(out); } + + @Override + public void write(ByteBuffer buffer) { + encrypted.write(buffer); + } } diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java index 048e7a6..179a475 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V4Pubkey.java @@ -24,6 +24,7 @@ import ch.dissem.bitmessage.utils.Decode; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.ByteBuffer; import java.util.Arrays; /** @@ -33,6 +34,8 @@ import java.util.Arrays; * to create messages to be used in spam or in flooding attacks. */ public class V4Pubkey extends Pubkey implements Encrypted { + private static final long serialVersionUID = 1556710353694033093L; + private long stream; private byte[] tag; private CryptoBox encrypted; @@ -83,11 +86,22 @@ public class V4Pubkey extends Pubkey implements Encrypted { encrypted.write(stream); } + @Override + public void write(ByteBuffer buffer) { + buffer.put(tag); + encrypted.write(buffer); + } + @Override public void writeUnencrypted(OutputStream out) throws IOException { decrypted.write(out); } + @Override + public void writeUnencrypted(ByteBuffer buffer) { + decrypted.write(buffer); + } + @Override public void writeBytesToSign(OutputStream out) throws IOException { out.write(tag); diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V5Broadcast.java b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V5Broadcast.java index c12aae0..8f07a30 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/payload/V5Broadcast.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/payload/V5Broadcast.java @@ -28,6 +28,8 @@ import java.io.OutputStream; * Users who are subscribed to the sending address will see the message appear in their inbox. */ public class V5Broadcast extends V4Broadcast { + private static final long serialVersionUID = 920649721626968644L; + private byte[] tag; private V5Broadcast(long stream, byte[] tag, CryptoBox encrypted) { diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/InventoryVector.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/InventoryVector.java index fc67422..9a3b258 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/InventoryVector.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/InventoryVector.java @@ -22,9 +22,12 @@ import ch.dissem.bitmessage.utils.Strings; import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; +import java.nio.ByteBuffer; import java.util.Arrays; public class InventoryVector implements Streamable, Serializable { + private static final long serialVersionUID = -7349009673063348719L; + /** * Hash of the object */ @@ -42,7 +45,7 @@ public class InventoryVector implements Streamable, Serializable { @Override public int hashCode() { - return hash != null ? Arrays.hashCode(hash) : 0; + return hash == null ? 0 : Arrays.hashCode(hash); } public byte[] getHash() { @@ -54,8 +57,13 @@ public class InventoryVector implements Streamable, Serializable { } @Override - public void write(OutputStream stream) throws IOException { - stream.write(hash); + public void write(OutputStream out) throws IOException { + out.write(hash); + } + + @Override + public void write(ByteBuffer buffer) { + buffer.put(hash); } @Override diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Label.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Label.java index 02f0384..facbe55 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Label.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/Label.java @@ -20,6 +20,8 @@ import java.io.Serializable; import java.util.Objects; public class Label implements Serializable { + private static final long serialVersionUID = 831782893630994914L; + private Object id; private String label; private Type type; 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 794ae44..94bc7c0 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,37 +17,43 @@ package ch.dissem.bitmessage.entity.valueobject; import ch.dissem.bitmessage.entity.Streamable; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.utils.Encode; import ch.dissem.bitmessage.utils.UnixTime; import java.io.IOException; import java.io.OutputStream; import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; import java.net.UnknownHostException; +import java.nio.ByteBuffer; import java.util.Arrays; /** * A node's address. It's written in IPv6 format. */ public class NetworkAddress implements Streamable { + private static final long serialVersionUID = 2500120578167100300L; + private long time; /** * Stream number for this node */ - private long stream; + private final long stream; /** * same service(s) listed in version */ - private long services; + private final long services; /** * IPv6 address. IPv4 addresses are written into the message as a 16 byte IPv4-mapped IPv6 address * (12 bytes 00 00 00 00 00 00 00 00 00 00 FF FF, followed by the 4 bytes of the IPv4 address). */ - private byte[] ipv6; - private int port; + private final byte[] ipv6; + private final int port; private NetworkAddress(Builder builder) { time = builder.time; @@ -85,7 +91,7 @@ public class NetworkAddress implements Streamable { try { return InetAddress.getByAddress(ipv6); } catch (UnknownHostException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -116,14 +122,29 @@ public class NetworkAddress implements Streamable { write(stream, false); } - public void write(OutputStream stream, boolean light) throws IOException { + public void write(OutputStream out, boolean light) throws IOException { if (!light) { - Encode.int64(time, stream); - Encode.int32(this.stream, stream); + Encode.int64(time, out); + Encode.int32(stream, out); } - Encode.int64(services, stream); - stream.write(ipv6); - Encode.int16(port, stream); + Encode.int64(services, out); + out.write(ipv6); + Encode.int16(port, out); + } + + @Override + public void write(ByteBuffer buffer) { + write(buffer, false); + } + + public void write(ByteBuffer buffer, boolean light) { + if (!light) { + Encode.int64(time, buffer); + Encode.int32(stream, buffer); + } + Encode.int64(services, buffer); + buffer.put(ipv6); + Encode.int16(port, buffer); } public static final class Builder { @@ -133,9 +154,6 @@ public class NetworkAddress implements Streamable { private byte[] ipv6; private int port; - public Builder() { - } - public Builder time(final long time) { this.time = time; return this; @@ -199,6 +217,17 @@ public class NetworkAddress implements Streamable { return this; } + public Builder address(SocketAddress address) { + if (address instanceof InetSocketAddress) { + InetSocketAddress inetAddress = (InetSocketAddress) address; + ip(inetAddress.getAddress()); + port(inetAddress.getPort()); + } else { + throw new IllegalArgumentException("Unknown type of address: " + address.getClass()); + } + return this; + } + public NetworkAddress build() { if (time == 0) { time = UnixTime.now(); diff --git a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/PrivateKey.java b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/PrivateKey.java index d07c859..7621ca5 100644 --- a/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/PrivateKey.java +++ b/core/src/main/java/ch/dissem/bitmessage/entity/valueobject/PrivateKey.java @@ -16,23 +16,32 @@ package ch.dissem.bitmessage.entity.valueobject; +import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Streamable; import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.utils.Bytes; import ch.dissem.bitmessage.utils.Decode; import ch.dissem.bitmessage.utils.Encode; import java.io.*; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; /** * Represents a private key. Additional information (stream, version, features, ...) is stored in the accompanying * {@link Pubkey} object. */ public class PrivateKey implements Streamable { + private static final long serialVersionUID = 8562555470709110558L; + public static final int PRIVATE_KEY_SIZE = 32; + private final byte[] privateSigningKey; private final byte[] privateEncryptionKey; @@ -45,15 +54,15 @@ public class PrivateKey implements Streamable { byte[] pubEK; byte[] ripe; do { - privSK = security().randomBytes(PRIVATE_KEY_SIZE); - privEK = security().randomBytes(PRIVATE_KEY_SIZE); - pubSK = security().createPublicKey(privSK); - pubEK = security().createPublicKey(privEK); + privSK = cryptography().randomBytes(PRIVATE_KEY_SIZE); + privEK = cryptography().randomBytes(PRIVATE_KEY_SIZE); + pubSK = cryptography().createPublicKey(privSK); + pubEK = cryptography().createPublicKey(privEK); ripe = Pubkey.getRipe(pubSK, pubEK); } while (ripe[0] != 0 || (shorter && ripe[1] != 0)); this.privateSigningKey = privSK; this.privateEncryptionKey = privEK; - this.pubkey = security().createPubkey(Pubkey.LATEST_VERSION, stream, privateSigningKey, privateEncryptionKey, + this.pubkey = cryptography().createPubkey(Pubkey.LATEST_VERSION, stream, privateSigningKey, privateEncryptionKey, nonceTrialsPerByte, extraBytes, features); } @@ -63,16 +72,74 @@ public class PrivateKey implements Streamable { this.pubkey = pubkey; } - public PrivateKey(long version, long stream, String passphrase, long nonceTrialsPerByte, long extraBytes, Pubkey.Feature... features) { - try { - // FIXME: this is most definitely wrong - this.privateSigningKey = Bytes.truncate(security().sha512(passphrase.getBytes("UTF-8"), new byte[]{0}), 32); - this.privateEncryptionKey = Bytes.truncate(security().sha512(passphrase.getBytes("UTF-8"), new byte[]{1}), 32); - this.pubkey = security().createPubkey(version, stream, privateSigningKey, privateEncryptionKey, - nonceTrialsPerByte, extraBytes, features); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); + public PrivateKey(BitmessageAddress address, String passphrase) { + this(address.getVersion(), address.getStream(), passphrase); + } + + public PrivateKey(long version, long stream, String passphrase) { + this(new Builder(version, stream, false).seed(passphrase).generate()); + } + + private PrivateKey(Builder builder) { + this.privateSigningKey = builder.privSK; + this.privateEncryptionKey = builder.privEK; + this.pubkey = Factory.createPubkey(builder.version, builder.stream, builder.pubSK, builder.pubEK, + InternalContext.NETWORK_NONCE_TRIALS_PER_BYTE, InternalContext.NETWORK_EXTRA_BYTES); + } + + private static class Builder { + final long version; + final long stream; + final boolean shorter; + + byte[] seed; + long nextNonce; + + byte[] privSK, privEK; + byte[] pubSK, pubEK; + + private Builder(long version, long stream, boolean shorter) { + this.version = version; + this.stream = stream; + this.shorter = shorter; } + + Builder seed(String passphrase) { + try { + seed = passphrase.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new ApplicationException(e); + } + return this; + } + + Builder generate() { + long signingKeyNonce = nextNonce; + long encryptionKeyNonce = nextNonce + 1; + byte[] ripe; + do { + privEK = Bytes.truncate(cryptography().sha512(seed, Encode.varInt(encryptionKeyNonce)), 32); + privSK = Bytes.truncate(cryptography().sha512(seed, Encode.varInt(signingKeyNonce)), 32); + pubSK = cryptography().createPublicKey(privSK); + pubEK = cryptography().createPublicKey(privEK); + ripe = cryptography().ripemd160(cryptography().sha512(pubSK, pubEK)); + + signingKeyNonce += 2; + encryptionKeyNonce += 2; + } while (ripe[0] != 0 || (shorter && ripe[1] != 0)); + nextNonce = signingKeyNonce; + return this; + } + } + + public static List<PrivateKey> deterministic(String passphrase, int numberOfAddresses, long version, long stream, boolean shorter) { + List<PrivateKey> result = new ArrayList<>(numberOfAddresses); + Builder builder = new Builder(version, stream, shorter).seed(passphrase); + for (int i = 0; i < numberOfAddresses; i++) { + builder.generate(); + result.add(new PrivateKey(builder)); + } + return result; } public static PrivateKey read(InputStream is) throws IOException { @@ -112,4 +179,20 @@ public class PrivateKey implements Streamable { Encode.varInt(privateEncryptionKey.length, out); out.write(privateEncryptionKey); } + + + @Override + public void write(ByteBuffer buffer) { + Encode.varInt(pubkey.getVersion(), buffer); + Encode.varInt(pubkey.getStream(), buffer); + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + pubkey.writeUnencrypted(baos); + Encode.varBytes(baos.toByteArray(), buffer); + } catch (IOException e) { + throw new ApplicationException(e); + } + Encode.varBytes(privateSigningKey, buffer); + Encode.varBytes(privateEncryptionKey, buffer); + } } diff --git a/core/src/main/java/ch/dissem/bitmessage/exception/AddressFormatException.java b/core/src/main/java/ch/dissem/bitmessage/exception/AddressFormatException.java index 7da990a..9f6674d 100644 --- a/core/src/main/java/ch/dissem/bitmessage/exception/AddressFormatException.java +++ b/core/src/main/java/ch/dissem/bitmessage/exception/AddressFormatException.java @@ -20,6 +20,8 @@ package ch.dissem.bitmessage.exception; * Indicates an illegal Bitmessage address */ public class AddressFormatException extends RuntimeException { + private static final long serialVersionUID = 6943764578672021573L; + public AddressFormatException(String message) { super(message); } diff --git a/core/src/main/java/ch/dissem/bitmessage/exception/ApplicationException.java b/core/src/main/java/ch/dissem/bitmessage/exception/ApplicationException.java new file mode 100644 index 0000000..e688cb1 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/exception/ApplicationException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016 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.exception; + +/** + * @author Christian Basler + */ +public class ApplicationException extends RuntimeException { + private static final long serialVersionUID = 1796776684126759324L; + + public ApplicationException(Throwable cause) { + super(cause); + } + + public ApplicationException(String message) { + super(message); + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/exception/DecryptionFailedException.java b/core/src/main/java/ch/dissem/bitmessage/exception/DecryptionFailedException.java index c68a050..c9e3efd 100644 --- a/core/src/main/java/ch/dissem/bitmessage/exception/DecryptionFailedException.java +++ b/core/src/main/java/ch/dissem/bitmessage/exception/DecryptionFailedException.java @@ -17,4 +17,5 @@ package ch.dissem.bitmessage.exception; public class DecryptionFailedException extends Exception { + private static final long serialVersionUID = 3241116253113872731L; } diff --git a/core/src/main/java/ch/dissem/bitmessage/exception/InsufficientProofOfWorkException.java b/core/src/main/java/ch/dissem/bitmessage/exception/InsufficientProofOfWorkException.java index 685e710..94a5d84 100644 --- a/core/src/main/java/ch/dissem/bitmessage/exception/InsufficientProofOfWorkException.java +++ b/core/src/main/java/ch/dissem/bitmessage/exception/InsufficientProofOfWorkException.java @@ -22,6 +22,8 @@ import java.io.IOException; import java.util.Arrays; public class InsufficientProofOfWorkException extends IOException { + private static final long serialVersionUID = 9105580366564571318L; + public InsufficientProofOfWorkException(byte[] target, byte[] hash) { super("Insufficient proof of work: " + Strings.hex(target) + " required, " + Strings.hex(Arrays.copyOfRange(hash, 0, 8)) + " achieved."); } diff --git a/core/src/main/java/ch/dissem/bitmessage/exception/NodeException.java b/core/src/main/java/ch/dissem/bitmessage/exception/NodeException.java index 9ab2b7f..cecd950 100644 --- a/core/src/main/java/ch/dissem/bitmessage/exception/NodeException.java +++ b/core/src/main/java/ch/dissem/bitmessage/exception/NodeException.java @@ -22,6 +22,8 @@ package ch.dissem.bitmessage.exception; * @author Ch. Basler */ public class NodeException extends RuntimeException { + private static final long serialVersionUID = 2965325796118227802L; + public NodeException(String message) { super(message); } diff --git a/core/src/main/java/ch/dissem/bitmessage/factory/BufferPool.java b/core/src/main/java/ch/dissem/bitmessage/factory/BufferPool.java new file mode 100644 index 0000000..65c1d34 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/factory/BufferPool.java @@ -0,0 +1,92 @@ +/* + * Copyright 2016 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.factory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Stack; +import java.util.TreeMap; + +import static ch.dissem.bitmessage.ports.NetworkHandler.HEADER_SIZE; +import static ch.dissem.bitmessage.ports.NetworkHandler.MAX_PAYLOAD_SIZE; + +/** + * A pool for {@link ByteBuffer}s. As they may use up a lot of memory, + * they should be reused as efficiently as possible. + */ +class BufferPool { + private static final Logger LOG = LoggerFactory.getLogger(BufferPool.class); + + public static final BufferPool bufferPool = new BufferPool(); + + private final Map<Integer, Stack<ByteBuffer>> pools = new TreeMap<>(); + + private BufferPool() { + pools.put(HEADER_SIZE, new Stack<ByteBuffer>()); + pools.put(54, new Stack<ByteBuffer>()); + pools.put(1000, new Stack<ByteBuffer>()); + pools.put(60000, new Stack<ByteBuffer>()); + pools.put(MAX_PAYLOAD_SIZE, new Stack<ByteBuffer>()); + } + + public synchronized ByteBuffer allocate(int capacity) { + Integer targetSize = getTargetSize(capacity); + Stack<ByteBuffer> pool = pools.get(targetSize); + if (pool.isEmpty()) { + LOG.trace("Creating new buffer of size " + targetSize); + return ByteBuffer.allocate(targetSize); + } else { + return pool.pop(); + } + } + + /** + * Returns a buffer that has the size of the Bitmessage network message header, 24 bytes. + * + * @return a buffer of size 24 + */ + public synchronized ByteBuffer allocateHeaderBuffer() { + Stack<ByteBuffer> pool = pools.get(HEADER_SIZE); + if (pool.isEmpty()) { + return ByteBuffer.allocate(HEADER_SIZE); + } else { + return pool.pop(); + } + } + + public synchronized void deallocate(ByteBuffer buffer) { + buffer.clear(); + Stack<ByteBuffer> pool = pools.get(buffer.capacity()); + if (pool == null) { + throw new IllegalArgumentException("Illegal buffer capacity " + buffer.capacity() + + " one of " + pools.keySet() + " expected."); + } else { + pool.push(buffer); + } + } + + private Integer getTargetSize(int capacity) { + for (Integer size : pools.keySet()) { + if (size >= capacity) return size; + } + throw new IllegalArgumentException("Requested capacity too large: " + + "requested=" + capacity + "; max=" + MAX_PAYLOAD_SIZE); + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/factory/Factory.java b/core/src/main/java/ch/dissem/bitmessage/factory/Factory.java index 33604ab..07b3161 100644 --- a/core/src/main/java/ch/dissem/bitmessage/factory/Factory.java +++ b/core/src/main/java/ch/dissem/bitmessage/factory/Factory.java @@ -23,6 +23,8 @@ import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.payload.*; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.exception.NodeException; +import ch.dissem.bitmessage.utils.TTL; +import ch.dissem.bitmessage.utils.UnixTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,13 +33,14 @@ import java.io.InputStream; import java.net.SocketException; import java.net.SocketTimeoutException; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.entity.payload.ObjectType.MSG; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; /** * Creates {@link NetworkMessage} objects from {@link InputStream InputStreams} */ public class Factory { - public static final Logger LOG = LoggerFactory.getLogger(Factory.class); + private static final Logger LOG = LoggerFactory.getLogger(Factory.class); public static NetworkMessage getNetworkMessage(int version, InputStream stream) throws SocketTimeoutException { try { @@ -116,8 +119,8 @@ public class Factory { BitmessageAddress temp = new BitmessageAddress(address); PrivateKey privateKey = new PrivateKey(privateSigningKey, privateEncryptionKey, createPubkey(temp.getVersion(), temp.getStream(), - security().createPublicKey(privateSigningKey), - security().createPublicKey(privateEncryptionKey), + cryptography().createPublicKey(privateSigningKey), + cryptography().createPublicKey(privateEncryptionKey), nonceTrialsPerByte, extraBytes, behaviourBitfield)); BitmessageAddress result = new BitmessageAddress(privateKey); if (!result.getAddress().equals(address)) { @@ -155,7 +158,7 @@ public class Factory { } // fallback: just store the message - we don't really care what it is LOG.trace("Unexpected object type: " + objectType); - return GenericPayload.read(version, stream, streamNumber, length); + return GenericPayload.read(version, streamNumber, stream, length); } private static ObjectPayload parseGetPubkey(long version, long streamNumber, InputStream stream, int length) throws IOException { @@ -177,7 +180,7 @@ public class Factory { private static ObjectPayload parsePubkey(long version, long streamNumber, InputStream stream, int length) throws IOException { Pubkey pubkey = readPubkey(version, streamNumber, stream, length, true); - return pubkey != null ? pubkey : GenericPayload.read(version, stream, streamNumber, length); + return pubkey != null ? pubkey : GenericPayload.read(version, streamNumber, stream, length); } private static ObjectPayload parseMsg(long version, long streamNumber, InputStream stream, int length) throws IOException { @@ -192,15 +195,23 @@ public class Factory { return V5Broadcast.read(stream, streamNumber, length); default: LOG.debug("Encountered unknown broadcast version " + version); - return GenericPayload.read(version, stream, streamNumber, length); + return GenericPayload.read(version, streamNumber, stream, length); } } - public static ObjectPayload getBroadcast(BitmessageAddress sendingAddress, Plaintext plaintext) { + public static Broadcast getBroadcast(Plaintext plaintext) { + BitmessageAddress sendingAddress = plaintext.getFrom(); if (sendingAddress.getVersion() < 4) { return new V4Broadcast(sendingAddress, plaintext); } else { return new V5Broadcast(sendingAddress, plaintext); } } + + public static ObjectMessage createAck(Plaintext plaintext) { + if (plaintext == null || plaintext.getAckData() == null) + return null; + GenericPayload ack = new GenericPayload(3, plaintext.getFrom().getStream(), plaintext.getAckData()); + return new ObjectMessage.Builder().objectType(MSG).payload(ack).expiresTime(UnixTime.now(plaintext.getTTL())).build(); + } } 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 d13e73e..7b27d13 100644 --- a/core/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java +++ b/core/src/main/java/ch/dissem/bitmessage/factory/V3MessageFactory.java @@ -32,7 +32,7 @@ import java.io.IOException; import java.io.InputStream; import static ch.dissem.bitmessage.entity.NetworkMessage.MAGIC_BYTES; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; /** * Creates protocol v3 network messages from {@link InputStream InputStreams} @@ -62,7 +62,7 @@ class V3MessageFactory { } } - private static MessagePayload getPayload(String command, InputStream stream, int length) throws IOException { + static MessagePayload getPayload(String command, InputStream stream, int length) throws IOException { switch (command) { case "version": return parseVersion(stream); @@ -107,12 +107,12 @@ class V3MessageFactory { } return new ObjectMessage.Builder() - .nonce(nonce) - .expiresTime(expiresTime) - .objectType(objectType) - .stream(stream) - .payload(payload) - .build(); + .nonce(nonce) + .expiresTime(expiresTime) + .objectType(objectType) + .stream(stream) + .payload(payload) + .build(); } private static GetData parseGetData(InputStream stream) throws IOException { @@ -153,13 +153,13 @@ class V3MessageFactory { long[] streamNumbers = Decode.varIntList(stream); return new Version.Builder() - .version(version) - .services(services) - .timestamp(timestamp) - .addrRecv(addrRecv).addrFrom(addrFrom) - .nonce(nonce) - .userAgent(userAgent) - .streams(streamNumbers).build(); + .version(version) + .services(services) + .timestamp(timestamp) + .addrRecv(addrRecv).addrFrom(addrFrom) + .nonce(nonce) + .userAgent(userAgent) + .streams(streamNumbers).build(); } private static InventoryVector parseInventoryVector(InputStream stream) throws IOException { @@ -179,11 +179,17 @@ class V3MessageFactory { long services = Decode.int64(stream); byte[] ipv6 = Decode.bytes(stream, 16); int port = Decode.uint16(stream); - return new NetworkAddress.Builder().time(time).stream(streamNumber).services(services).ipv6(ipv6).port(port).build(); + return new NetworkAddress.Builder() + .time(time) + .stream(streamNumber) + .services(services) + .ipv6(ipv6) + .port(port) + .build(); } private static boolean testChecksum(byte[] checksum, byte[] payload) { - byte[] payloadChecksum = security().sha512(payload); + byte[] payloadChecksum = cryptography().sha512(payload); for (int i = 0; i < checksum.length; i++) { if (checksum[i] != payloadChecksum[i]) { return false; diff --git a/core/src/main/java/ch/dissem/bitmessage/factory/V3MessageReader.java b/core/src/main/java/ch/dissem/bitmessage/factory/V3MessageReader.java new file mode 100644 index 0000000..89677cd --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/factory/V3MessageReader.java @@ -0,0 +1,189 @@ +/* + * Copyright 2016 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.factory; + +import ch.dissem.bitmessage.entity.MessagePayload; +import ch.dissem.bitmessage.entity.NetworkMessage; +import ch.dissem.bitmessage.exception.ApplicationException; +import ch.dissem.bitmessage.exception.NodeException; +import ch.dissem.bitmessage.utils.Decode; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; + +import static ch.dissem.bitmessage.entity.NetworkMessage.MAGIC_BYTES; +import static ch.dissem.bitmessage.factory.BufferPool.bufferPool; +import static ch.dissem.bitmessage.ports.NetworkHandler.MAX_PAYLOAD_SIZE; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; + +/** + * Similar to the {@link V3MessageFactory}, but used for NIO buffers which may or may not contain a whole message. + */ +public class V3MessageReader { + private ByteBuffer headerBuffer; + private ByteBuffer dataBuffer; + + private ReaderState state = ReaderState.MAGIC; + private String command; + private int length; + private byte[] checksum; + + private List<NetworkMessage> messages = new LinkedList<>(); + + public ByteBuffer getActiveBuffer() { + if (state != null && state != ReaderState.DATA) { + if (headerBuffer == null) { + headerBuffer = bufferPool.allocateHeaderBuffer(); + } + } + return state == ReaderState.DATA ? dataBuffer : headerBuffer; + } + + public void update() { + if (state != ReaderState.DATA) { + getActiveBuffer(); + headerBuffer.flip(); + } + switch (state) { + case MAGIC: + if (!findMagicBytes(headerBuffer)) { + headerBuffer.compact(); + return; + } + state = ReaderState.HEADER; + case HEADER: + if (headerBuffer.remaining() < 20) { + headerBuffer.compact(); + headerBuffer.limit(20); + return; + } + command = getCommand(headerBuffer); + length = (int) Decode.uint32(headerBuffer); + if (length > MAX_PAYLOAD_SIZE) { + throw new NodeException("Payload of " + length + " bytes received, no more than " + + MAX_PAYLOAD_SIZE + " was expected."); + } + checksum = new byte[4]; + headerBuffer.get(checksum); + state = ReaderState.DATA; + bufferPool.deallocate(headerBuffer); + headerBuffer = null; + dataBuffer = bufferPool.allocate(length); + dataBuffer.clear(); + dataBuffer.limit(length); + case DATA: + if (dataBuffer.position() < length) { + return; + } else { + dataBuffer.flip(); + } + if (!testChecksum(dataBuffer)) { + state = ReaderState.MAGIC; + throw new NodeException("Checksum failed for message '" + command + "'"); + } + try { + MessagePayload payload = V3MessageFactory.getPayload( + command, + new ByteArrayInputStream(dataBuffer.array(), + dataBuffer.arrayOffset() + dataBuffer.position(), length), + length); + if (payload != null) { + messages.add(new NetworkMessage(payload)); + } + } catch (IOException e) { + throw new NodeException(e.getMessage()); + } finally { + state = ReaderState.MAGIC; + bufferPool.deallocate(dataBuffer); + dataBuffer = null; + dataBuffer = null; + } + } + } + + public List<NetworkMessage> getMessages() { + return messages; + } + + private boolean findMagicBytes(ByteBuffer buffer) { + int i = 0; + while (buffer.hasRemaining()) { + if (i == 0) { + buffer.mark(); + } + if (buffer.get() == MAGIC_BYTES[i]) { + i++; + if (i == MAGIC_BYTES.length) { + return true; + } + } else { + i = 0; + } + } + if (i > 0) { + buffer.reset(); + } + return false; + } + + private static String getCommand(ByteBuffer buffer) { + int start = buffer.position(); + int l = 0; + while (l < 12 && buffer.get() != 0) l++; + int i = l + 1; + while (i < 12) { + if (buffer.get() != 0) throw new NodeException("'\\0' padding expected for command"); + i++; + } + try { + return new String(buffer.array(), start, l, "ASCII"); + } catch (UnsupportedEncodingException e) { + throw new ApplicationException(e); + } + } + + private boolean testChecksum(ByteBuffer buffer) { + byte[] payloadChecksum = cryptography().sha512(buffer.array(), + buffer.arrayOffset() + buffer.position(), length); + for (int i = 0; i < checksum.length; i++) { + if (checksum[i] != payloadChecksum[i]) { + return false; + } + } + return true; + } + + /** + * De-allocates all buffers. This method should be called iff the reader isn't used anymore, i.e. when its + * connection is severed. + */ + public void cleanup() { + state = null; + if (headerBuffer != null) { + bufferPool.deallocate(headerBuffer); + } + if (dataBuffer != null) { + bufferPool.deallocate(dataBuffer); + } + } + + private enum ReaderState {MAGIC, HEADER, DATA} +} 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 3cf3f0d..b02b12d 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/AbstractCryptography.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/AbstractCryptography.java @@ -19,6 +19,7 @@ package ch.dissem.bitmessage.ports; import ch.dissem.bitmessage.InternalContext; import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.utils.Bytes; @@ -34,13 +35,15 @@ import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.security.SecureRandom; +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.utils.Numbers.max; /** * Implements everything that isn't directly dependent on either Spongy- or Bouncycastle. */ public abstract class AbstractCryptography implements Cryptography, InternalContext.ContextHolder { - public static final Logger LOG = LoggerFactory.getLogger(Cryptography.class); + protected static final Logger LOG = LoggerFactory.getLogger(Cryptography.class); private static final SecureRandom RANDOM = new SecureRandom(); private static final BigInteger TWO = BigInteger.valueOf(2); private static final BigInteger TWO_POW_64 = TWO.pow(64); @@ -58,6 +61,12 @@ public abstract class AbstractCryptography implements Cryptography, InternalCont this.context = context; } + public byte[] sha512(byte[] data, int offset, int length) { + MessageDigest mda = md("SHA-512"); + mda.update(data, offset, length); + return mda.digest(); + } + public byte[] sha512(byte[]... data) { return hash("SHA-512", data); } @@ -98,8 +107,8 @@ public abstract class AbstractCryptography implements Cryptography, InternalCont public void doProofOfWork(ObjectMessage object, long nonceTrialsPerByte, long extraBytes, ProofOfWorkEngine.Callback callback) { - nonceTrialsPerByte = max(nonceTrialsPerByte, context.getNetworkNonceTrialsPerByte()); - extraBytes = max(extraBytes, context.getNetworkExtraBytes()); + nonceTrialsPerByte = max(nonceTrialsPerByte, NETWORK_NONCE_TRIALS_PER_BYTE); + extraBytes = max(extraBytes, NETWORK_EXTRA_BYTES); byte[] initialHash = getInitialHash(object); @@ -124,8 +133,8 @@ public abstract class AbstractCryptography implements Cryptography, InternalCont @Override public byte[] getProofOfWorkTarget(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { - if (nonceTrialsPerByte == 0) nonceTrialsPerByte = context.getNetworkNonceTrialsPerByte(); - if (extraBytes == 0) extraBytes = context.getNetworkExtraBytes(); + if (nonceTrialsPerByte == 0) nonceTrialsPerByte = NETWORK_NONCE_TRIALS_PER_BYTE; + if (extraBytes == 0) extraBytes = NETWORK_EXTRA_BYTES; BigInteger TTL = BigInteger.valueOf(object.getExpiresTime() - UnixTime.now()); BigInteger numerator = TWO_POW_64; @@ -151,7 +160,7 @@ public abstract class AbstractCryptography implements Cryptography, InternalCont try { return MessageDigest.getInstance(algorithm, provider); } catch (GeneralSecurityException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -161,7 +170,7 @@ public abstract class AbstractCryptography implements Cryptography, InternalCont mac.init(new SecretKeySpec(key_m, "HmacSHA256")); return mac.doFinal(data); } catch (GeneralSecurityException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/AbstractMessageRepository.java b/core/src/main/java/ch/dissem/bitmessage/ports/AbstractMessageRepository.java new file mode 100644 index 0000000..e0448c7 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/ports/AbstractMessageRepository.java @@ -0,0 +1,127 @@ +/* + * Copyright 2016 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.ports; + +import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.Plaintext; +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.List; + +import static ch.dissem.bitmessage.utils.SqlStrings.join; + +public abstract class AbstractMessageRepository implements MessageRepository, InternalContext.ContextHolder { + protected InternalContext ctx; + + @Override + public void setContext(InternalContext context) { + this.ctx = context; + } + + protected void safeSenderIfNecessary(Plaintext message) { + if (message.getId() == null) { + BitmessageAddress savedAddress = ctx.getAddressRepository().getAddress(message.getFrom().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(savedAddress); + } + } + } + + @Override + public Plaintext getMessage(Object id) { + if (id instanceof Long) { + return single(find("id=" + id)); + } else { + throw new IllegalArgumentException("Long expected for ID"); + } + } + + @Override + public Plaintext getMessage(byte[] initialHash) { + return single(find("initial_hash=X'" + Strings.hex(initialHash) + "'")); + } + + @Override + public Plaintext getMessageForAck(byte[] ackData) { + return single(find("ack_data=X'" + Strings.hex(ackData) + "' AND status='" + Plaintext.Status.SENT + "'")); + } + + @Override + public List<Plaintext> findMessages(Label label) { + if (label == null) { + return find("id NOT IN (SELECT message_id FROM Message_Label)"); + } else { + return find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")"); + } + } + + @Override + public List<Plaintext> findMessages(Plaintext.Status status, BitmessageAddress recipient) { + return find("status='" + status.name() + "' AND recipient='" + recipient.getAddress() + "'"); + } + + @Override + public List<Plaintext> findMessages(Plaintext.Status status) { + return find("status='" + status.name() + "'"); + } + + @Override + public List<Plaintext> findMessages(BitmessageAddress sender) { + return find("sender='" + sender.getAddress() + "'"); + } + + @Override + public List<Plaintext> findMessagesToResend() { + return find("status='" + Plaintext.Status.SENT.name() + "'" + + " AND next_try < " + UnixTime.now()); + } + + @Override + public List<Label> getLabels() { + return findLabels("1=1"); + } + + @Override + public List<Label> getLabels(Label.Type... types) { + return findLabels("type IN (" + join(types) + ")"); + } + + protected abstract List<Label> findLabels(String where); + + + protected <T> T single(Collection<T> collection) { + switch (collection.size()) { + case 0: + return null; + case 1: + return collection.iterator().next(); + default: + throw new ApplicationException("This shouldn't happen, found " + collection.size() + + " items, one or none was expected"); + } + } + + protected abstract List<Plaintext> find(String where); +} 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 2770997..ff397ba 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/AddressRepository.java @@ -30,12 +30,17 @@ public interface AddressRepository { */ List<BitmessageAddress> getIdentities(); + /** + * @return all subscribed chans. + */ + List<BitmessageAddress> getChans(); + List<BitmessageAddress> getSubscriptions(); List<BitmessageAddress> getSubscriptions(long broadcastVersion); /** - * @return all Bitmessage addresses that have no private key. + * @return all Bitmessage addresses that have no private key or are chans. */ List<BitmessageAddress> getContacts(); diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/Cryptography.java b/core/src/main/java/ch/dissem/bitmessage/ports/Cryptography.java index 48739ea..9ea6a9d 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/Cryptography.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/Cryptography.java @@ -30,6 +30,18 @@ import java.security.SecureRandom; * which should be secure enough. */ public interface Cryptography { + /** + * A helper method to calculate SHA-512 hashes. Please note that a new {@link MessageDigest} object is created at + * each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in + * success on the same thread. + * + * @param data to get hashed + * @param offset of the data to be hashed + * @param length of the data to be hashed + * @return SHA-512 hash of data within the given range + */ + byte[] sha512(byte[] data, int offset, int length); + /** * A helper method to calculate SHA-512 hashes. Please note that a new {@link MessageDigest} object is created at * each call (to ensure thread safety), so you shouldn't use this if you need to do many hash calculations in diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/DefaultLabeler.java b/core/src/main/java/ch/dissem/bitmessage/ports/DefaultLabeler.java new file mode 100644 index 0000000..e4c2105 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/ports/DefaultLabeler.java @@ -0,0 +1,92 @@ +/* + * Copyright 2016 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.ports; + +import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.valueobject.Label; + +import static ch.dissem.bitmessage.entity.Plaintext.Status.*; + +public class DefaultLabeler implements Labeler, InternalContext.ContextHolder { + private InternalContext ctx; + + @Override + public void setLabels(Plaintext msg) { + msg.setStatus(RECEIVED); + if (msg.getType() == Plaintext.Type.BROADCAST) { + msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.BROADCAST, Label.Type.UNREAD)); + } else { + msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.INBOX, Label.Type.UNREAD)); + } + } + + @Override + public void markAsDraft(Plaintext msg) { + msg.setStatus(DRAFT); + msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.DRAFT)); + } + + @Override + public void markAsSending(Plaintext msg) { + if (msg.getTo() != null && msg.getTo().getPubkey() == null) { + msg.setStatus(PUBKEY_REQUESTED); + } else { + msg.setStatus(DOING_PROOF_OF_WORK); + } + msg.removeLabel(Label.Type.DRAFT); + msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.OUTBOX)); + } + + @Override + public void markAsSent(Plaintext msg) { + msg.setStatus(SENT); + msg.removeLabel(Label.Type.OUTBOX); + msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.SENT)); + } + + @Override + public void markAsAcknowledged(Plaintext msg) { + msg.setStatus(SENT_ACKNOWLEDGED); + } + + @Override + public void markAsRead(Plaintext msg) { + msg.removeLabel(Label.Type.UNREAD); + } + + @Override + public void markAsUnread(Plaintext msg) { + msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.UNREAD)); + } + + @Override + public void delete(Plaintext msg) { + msg.getLabels().clear(); + msg.addLabels(ctx.getMessageRepository().getLabels(Label.Type.TRASH)); + } + + @Override + public void archive(Plaintext msg) { + msg.getLabels().clear(); + } + + @Override + public void setContext(InternalContext ctx) { + this.ctx = ctx; + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/Labeler.java b/core/src/main/java/ch/dissem/bitmessage/ports/Labeler.java new file mode 100644 index 0000000..e79bee2 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/ports/Labeler.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016 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.ports; + +import ch.dissem.bitmessage.entity.Plaintext; + +/** + * Defines and sets labels. Note that it should also update the status field of a message. + * Generally it's highly advised to override the {@link DefaultLabeler} whenever possible, + * instead of directly implementing the interface. + * <p> + * As the labeler gets called whenever the state of a message changes, it can also be used + * as a listener. + * </p> + */ +public interface Labeler { + /** + * Sets the labels of a newly received message. + * + * @param msg an unlabeled message or broadcast + */ + void setLabels(Plaintext msg); + + void markAsDraft(Plaintext msg); + + /** + * It is paramount that this methods marks the {@link Plaintext} object with status + * {@link Plaintext.Status#PUBKEY_REQUESTED} (see {@link DefaultLabeler}) + */ + void markAsSending(Plaintext msg); + + void markAsSent(Plaintext msg); + + void markAsAcknowledged(Plaintext msg); + + void markAsRead(Plaintext msg); + + void markAsUnread(Plaintext msg); + + void delete(Plaintext msg); + + void archive(Plaintext msg); +} diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/MemoryNodeRegistry.java b/core/src/main/java/ch/dissem/bitmessage/ports/MemoryNodeRegistry.java deleted file mode 100644 index 8d43423..0000000 --- a/core/src/main/java/ch/dissem/bitmessage/ports/MemoryNodeRegistry.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2015 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.ports; - -import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; -import ch.dissem.bitmessage.utils.UnixTime; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.net.InetAddress; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -import static ch.dissem.bitmessage.utils.Collections.selectRandom; -import static ch.dissem.bitmessage.utils.UnixTime.HOUR; -import static java.util.Collections.newSetFromMap; - -public class MemoryNodeRegistry implements NodeRegistry { - private static final Logger LOG = LoggerFactory.getLogger(MemoryNodeRegistry.class); - - private final Map<Long, Set<NetworkAddress>> stableNodes = new ConcurrentHashMap<>(); - private final Map<Long, Set<NetworkAddress>> knownNodes = new ConcurrentHashMap<>(); - - private void loadStableNodes() { - try (InputStream in = getClass().getClassLoader().getResourceAsStream("nodes.txt")) { - Scanner scanner = new Scanner(in); - long stream = 0; - Set<NetworkAddress> streamSet = null; - while (scanner.hasNext()) { - try { - String line = scanner.nextLine().trim(); - if (line.startsWith("#") || line.isEmpty()) { - // Ignore - continue; - } - if (line.startsWith("[stream")) { - stream = Long.parseLong(line.substring(8, line.lastIndexOf(']'))); - streamSet = new HashSet<>(); - stableNodes.put(stream, streamSet); - } else if (streamSet != null) { - int portIndex = line.lastIndexOf(':'); - InetAddress[] inetAddresses = InetAddress.getAllByName(line.substring(0, portIndex)); - int port = Integer.valueOf(line.substring(portIndex + 1)); - for (InetAddress inetAddress : inetAddresses) { - streamSet.add(new NetworkAddress.Builder().ip(inetAddress).port(port).stream(stream).build()); - } - } - } catch (IOException e) { - LOG.warn(e.getMessage(), e); - } - } - if (LOG.isDebugEnabled()) { - for (Map.Entry<Long, Set<NetworkAddress>> e : stableNodes.entrySet()) { - LOG.debug("Stream " + e.getKey() + ": loaded " + e.getValue().size() + " bootstrap nodes."); - } - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public List<NetworkAddress> getKnownAddresses(int limit, long... streams) { - List<NetworkAddress> result = new LinkedList<>(); - for (long stream : streams) { - Set<NetworkAddress> known = knownNodes.get(stream); - if (known != null && !known.isEmpty()) { - for (NetworkAddress node : known) { - if (node.getTime() > UnixTime.now(-3 * HOUR)) { - result.add(node); - } else { - known.remove(node); - } - } - } else { - Set<NetworkAddress> nodes = stableNodes.get(stream); - if (nodes == null || nodes.isEmpty()) { - loadStableNodes(); - nodes = stableNodes.get(stream); - } - if (nodes != null && !nodes.isEmpty()) { - // To reduce load on stable nodes, only return one - result.add(selectRandom(nodes)); - } - } - } - return selectRandom(limit, result); - } - - @Override - public void offerAddresses(List<NetworkAddress> addresses) { - for (NetworkAddress node : addresses) { - if (node.getTime() <= UnixTime.now()) { - if (!knownNodes.containsKey(node.getStream())) { - synchronized (knownNodes) { - if (!knownNodes.containsKey(node.getStream())) { - knownNodes.put( - node.getStream(), - newSetFromMap(new ConcurrentHashMap<NetworkAddress, Boolean>()) - ); - } - } - } - if (node.getTime() <= UnixTime.now()) { - // TODO: This isn't quite correct - // If the node is already known, the one with the more recent time should be used - knownNodes.get(node.getStream()).add(node); - } - } - } - } -} 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 9e949a7..aca16b9 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/MessageRepository.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/MessageRepository.java @@ -30,8 +30,12 @@ public interface MessageRepository { int countUnread(Label label); + Plaintext getMessage(Object id); + Plaintext getMessage(byte[] initialHash); + Plaintext getMessageForAck(byte[] ackData); + List<Plaintext> findMessages(Label label); List<Plaintext> findMessages(Status status); @@ -40,6 +44,8 @@ public interface MessageRepository { List<Plaintext> findMessages(BitmessageAddress sender); + List<Plaintext> findMessagesToResend(); + void save(Plaintext message); void remove(Plaintext message); diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/MultiThreadedPOWEngine.java b/core/src/main/java/ch/dissem/bitmessage/ports/MultiThreadedPOWEngine.java index 790e3b7..fb45e50 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/MultiThreadedPOWEngine.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/MultiThreadedPOWEngine.java @@ -16,6 +16,7 @@ package ch.dissem.bitmessage.ports; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.utils.Bytes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,16 +25,18 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.Semaphore; +import java.util.concurrent.*; import static ch.dissem.bitmessage.utils.Bytes.inc; +import static ch.dissem.bitmessage.utils.ThreadFactoryBuilder.pool; /** * A POW engine using all available CPU cores. */ public class MultiThreadedPOWEngine implements ProofOfWorkEngine { private static final Logger LOG = LoggerFactory.getLogger(MultiThreadedPOWEngine.class); - private static final Semaphore semaphore = new Semaphore(1, true); + private final ExecutorService waiterPool = Executors.newSingleThreadExecutor(pool("POW-waiter").daemon().build()); + private final ExecutorService workerPool = Executors.newCachedThreadPool(pool("POW-worker").daemon().build()); /** * This method will block until all pending nonce calculations are done, but not wait for its own calculation @@ -45,42 +48,59 @@ public class MultiThreadedPOWEngine implements ProofOfWorkEngine { * @param callback called with the calculated nonce as argument. The ProofOfWorkEngine implementation must make */ @Override - public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) { - try { - semaphore.acquire(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - callback = new CallbackWrapper(callback); - int cores = Runtime.getRuntime().availableProcessors(); - if (cores > 255) cores = 255; - LOG.info("Doing POW using " + cores + " cores"); - List<Worker> workers = new ArrayList<>(cores); - for (int i = 0; i < cores; i++) { - Worker w = new Worker(workers, (byte) cores, i, initialHash, target, callback); - workers.add(w); - } - for (Worker w : workers) { - // Doing this in the previous loop might cause a ConcurrentModificationException in the worker - // if a worker finds a nonce while new ones are still being added. - w.start(); - } + public void calculateNonce(final byte[] initialHash, final byte[] target, final Callback callback) { + waiterPool.execute(new Runnable() { + @Override + public void run() { + long startTime = System.currentTimeMillis(); + + int cores = Runtime.getRuntime().availableProcessors(); + if (cores > 255) cores = 255; + LOG.info("Doing POW using " + cores + " cores"); + List<Worker> workers = new ArrayList<>(cores); + for (int i = 0; i < cores; i++) { + Worker w = new Worker((byte) cores, i, initialHash, target); + workers.add(w); + } + List<Future<byte[]>> futures = new ArrayList<>(cores); + for (Worker w : workers) { + // Doing this in the previous loop might cause a ConcurrentModificationException in the worker + // if a worker finds a nonce while new ones are still being added. + futures.add(workerPool.submit(w)); + } + try { + while (!Thread.interrupted()) { + for (Future<byte[]> future : futures) { + if (future.isDone()) { + callback.onNonceCalculated(initialHash, future.get()); + LOG.info("Nonce calculated in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds"); + for (Future<byte[]> f : futures) { + f.cancel(true); + } + return; + } + } + Thread.sleep(100); + } + LOG.error("POW waiter thread interrupted - this should not happen!"); + } catch (ExecutionException e) { + LOG.error(e.getMessage(), e); + } catch (InterruptedException e) { + LOG.error("POW waiter thread interrupted - this should not happen!", e); + } + } + }); } - private static class Worker extends Thread { - private final Callback callback; + private class Worker implements Callable<byte[]> { private final byte numberOfCores; - private final List<Worker> workers; private final byte[] initialHash; private final byte[] target; private final MessageDigest mda; private final byte[] nonce = new byte[8]; - public Worker(List<Worker> workers, byte numberOfCores, int core, byte[] initialHash, byte[] target, - Callback callback) { - this.callback = callback; + Worker(byte numberOfCores, int core, byte[] initialHash, byte[] target) { this.numberOfCores = numberOfCores; - this.workers = workers; this.initialHash = initialHash; this.target = target; this.nonce[7] = (byte) core; @@ -88,54 +108,21 @@ public class MultiThreadedPOWEngine implements ProofOfWorkEngine { mda = MessageDigest.getInstance("SHA-512"); } catch (NoSuchAlgorithmException e) { LOG.error(e.getMessage(), e); - throw new RuntimeException(e); + throw new ApplicationException(e); } } @Override - public void run() { + public byte[] call() throws Exception { do { inc(nonce, numberOfCores); mda.update(nonce); mda.update(initialHash); if (!Bytes.lt(target, mda.digest(mda.digest()), 8)) { - synchronized (callback) { - if (!Thread.interrupted()) { - for (Worker w : workers) { - w.interrupt(); - } - // Clear interrupted flag for callback - Thread.interrupted(); - callback.onNonceCalculated(initialHash, nonce); - } - } - return; + return nonce; } } while (!Thread.interrupted()); - } - } - - public static class CallbackWrapper implements Callback { - private final Callback callback; - private final long startTime; - private boolean waiting = true; - - public CallbackWrapper(Callback callback) { - this.startTime = System.currentTimeMillis(); - this.callback = callback; - } - - @Override - public void onNonceCalculated(byte[] initialHash, byte[] nonce) { - // Prevents the callback from being called twice if two nonces are found simultaneously - synchronized (this) { - if (waiting) { - semaphore.release(); - LOG.info("Nonce calculated in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds"); - waiting = false; - callback.onNonceCalculated(initialHash, nonce); - } - } + return null; } } } diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java b/core/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java index 909d3dd..e9c164e 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/NetworkHandler.java @@ -23,19 +23,25 @@ import ch.dissem.bitmessage.utils.Property; import java.io.IOException; import java.net.InetAddress; +import java.util.Collection; import java.util.concurrent.Future; /** * Handles incoming messages */ public interface NetworkHandler { + int NETWORK_MAGIC_NUMBER = 8; + int HEADER_SIZE = 24; + int MAX_PAYLOAD_SIZE = 1600003; + int MAX_MESSAGE_SIZE = HEADER_SIZE + MAX_PAYLOAD_SIZE; + /** * Connects to the trusted host, fetches and offers new messages and disconnects afterwards. * <p> * An implementation should disconnect if either the timeout is reached or the returned thread is interrupted. * </p> */ - Future<?> synchronize(InetAddress server, int port, MessageListener listener, long timeoutInSeconds); + Future<?> synchronize(InetAddress server, int port, long timeoutInSeconds); /** * Send a custom message to a specific node (that should implement handling for this message type) and returns @@ -51,7 +57,7 @@ public interface NetworkHandler { /** * Start a full network node, accepting incoming connections and relaying objects. */ - void start(MessageListener listener); + void start(); /** * Stop the full network node. @@ -63,6 +69,13 @@ public interface NetworkHandler { */ void offer(InventoryVector iv); + /** + * Request each of those objects from a node that knows of the requested object. + * + * @param inventoryVectors of the objects to be requested + */ + void request(Collection<InventoryVector> inventoryVectors); + Property getNetworkStatus(); boolean isRunning(); diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/NodeRegistryHelper.java b/core/src/main/java/ch/dissem/bitmessage/ports/NodeRegistryHelper.java new file mode 100644 index 0000000..63d70eb --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/ports/NodeRegistryHelper.java @@ -0,0 +1,54 @@ +package ch.dissem.bitmessage.ports; + +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.exception.ApplicationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.util.*; + +/** + * Helper class to kick start node registries. + */ +public class NodeRegistryHelper { + private static final Logger LOG = LoggerFactory.getLogger(NodeRegistryHelper.class); + + public static Map<Long, Set<NetworkAddress>> loadStableNodes() { + try (InputStream in = NodeRegistryHelper.class.getClassLoader().getResourceAsStream("nodes.txt")) { + Scanner scanner = new Scanner(in); + long stream = 0; + Map<Long, Set<NetworkAddress>> result = new HashMap<>(); + Set<NetworkAddress> streamSet = null; + while (scanner.hasNext()) { + try { + String line = scanner.nextLine().trim(); + if (line.startsWith("[stream")) { + stream = Long.parseLong(line.substring(8, line.lastIndexOf(']'))); + streamSet = new HashSet<>(); + result.put(stream, streamSet); + } else if (streamSet != null && !line.isEmpty() && !line.startsWith("#")) { + int portIndex = line.lastIndexOf(':'); + InetAddress[] inetAddresses = InetAddress.getAllByName(line.substring(0, portIndex)); + int port = Integer.valueOf(line.substring(portIndex + 1)); + for (InetAddress inetAddress : inetAddresses) { + streamSet.add(new NetworkAddress.Builder().ip(inetAddress).port(port).stream(stream).build()); + } + } + } catch (IOException e) { + LOG.warn(e.getMessage(), e); + } + } + if (LOG.isDebugEnabled()) { + for (Map.Entry<Long, Set<NetworkAddress>> e : result.entrySet()) { + LOG.debug("Stream " + e.getKey() + ": loaded " + e.getValue().size() + " bootstrap nodes."); + } + } + return result; + } catch (IOException e) { + throw new ApplicationException(e); + } + } +} diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkRepository.java b/core/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkRepository.java index 739c172..b27a05d 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkRepository.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/ProofOfWorkRepository.java @@ -1,6 +1,7 @@ package ch.dissem.bitmessage.ports; import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.Plaintext; import java.util.List; @@ -16,6 +17,8 @@ public interface ProofOfWorkRepository { void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes); + void putObject(Item item); + void removeObject(byte[] initialHash); class Item { @@ -23,10 +26,20 @@ public interface ProofOfWorkRepository { public final long nonceTrialsPerByte; public final long extraBytes; + // Needed for ACK POW calculation + public final Long expirationTime; + public final Plaintext message; + public Item(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { + this(object, nonceTrialsPerByte, extraBytes, 0, null); + } + + public Item(ObjectMessage object, long nonceTrialsPerByte, long extraBytes, long expirationTime, Plaintext message) { this.object = object; this.nonceTrialsPerByte = nonceTrialsPerByte; this.extraBytes = extraBytes; + this.expirationTime = expirationTime; + this.message = message; } } } diff --git a/core/src/main/java/ch/dissem/bitmessage/ports/SimplePOWEngine.java b/core/src/main/java/ch/dissem/bitmessage/ports/SimplePOWEngine.java index e8d649b..a7d0d57 100644 --- a/core/src/main/java/ch/dissem/bitmessage/ports/SimplePOWEngine.java +++ b/core/src/main/java/ch/dissem/bitmessage/ports/SimplePOWEngine.java @@ -16,9 +16,11 @@ package ch.dissem.bitmessage.ports; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.utils.Bytes; import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import static ch.dissem.bitmessage.utils.Bytes.inc; @@ -32,18 +34,17 @@ import static ch.dissem.bitmessage.utils.Bytes.inc; public class SimplePOWEngine implements ProofOfWorkEngine { @Override public void calculateNonce(byte[] initialHash, byte[] target, Callback callback) { - byte[] nonce = new byte[8]; - MessageDigest mda; try { - mda = MessageDigest.getInstance("SHA-512"); - } catch (Exception e) { - throw new RuntimeException(e); + MessageDigest mda = MessageDigest.getInstance("SHA-512"); + byte[] nonce = new byte[8]; + do { + inc(nonce); + mda.update(nonce); + mda.update(initialHash); + } while (Bytes.lt(target, mda.digest(mda.digest()), 8)); + callback.onNonceCalculated(initialHash, nonce); + } catch (NoSuchAlgorithmException e) { + throw new ApplicationException(e); } - do { - inc(nonce); - mda.update(nonce); - mda.update(initialHash); - } while (Bytes.lt(target, mda.digest(mda.digest()), 8)); - callback.onNonceCalculated(initialHash, nonce); } } diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Base58.java b/core/src/main/java/ch/dissem/bitmessage/utils/Base58.java index ec2476a..a67e344 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Base58.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Base58.java @@ -18,6 +18,7 @@ package ch.dissem.bitmessage.utils; import ch.dissem.bitmessage.exception.AddressFormatException; +import ch.dissem.bitmessage.exception.ApplicationException; import java.io.UnsupportedEncodingException; @@ -30,7 +31,7 @@ import static java.util.Arrays.copyOfRange; */ public class Base58 { private static final int[] INDEXES = new int[128]; - private static char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray(); + private static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray(); static { for (int i = 0; i < INDEXES.length; i++) { @@ -44,27 +45,27 @@ public class Base58 { /** * Encodes the given bytes in base58. No checksum is appended. * - * @param input to encode + * @param data to encode * @return base58 encoded input */ - public static String encode(byte[] input) { - if (input.length == 0) { + public static String encode(byte[] data) { + if (data.length == 0) { return ""; } - input = copyOfRange(input, 0, input.length); + final byte[] bytes = copyOfRange(data, 0, data.length); // Count leading zeroes. int zeroCount = 0; - while (zeroCount < input.length && input[zeroCount] == 0) { + while (zeroCount < bytes.length && bytes[zeroCount] == 0) { ++zeroCount; } // The actual encoding. - byte[] temp = new byte[input.length * 2]; + byte[] temp = new byte[bytes.length * 2]; int j = temp.length; int startAt = zeroCount; - while (startAt < input.length) { - byte mod = divmod58(input, startAt); - if (input[startAt] == 0) { + while (startAt < bytes.length) { + byte mod = divmod58(bytes, startAt); + if (bytes[startAt] == 0) { ++startAt; } temp[--j] = (byte) ALPHABET[mod]; @@ -83,7 +84,7 @@ public class Base58 { try { return new String(output, "US-ASCII"); } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); // Cannot happen. + throw new ApplicationException(e); // Cannot happen. } } @@ -97,7 +98,7 @@ public class Base58 { char c = input.charAt(i); int digit58 = -1; - if (c >= 0 && c < 128) { + if (c < 128) { digit58 = INDEXES[c]; } if (digit58 < 0) { 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 8107eb0..986c288 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Bytes.java @@ -23,6 +23,8 @@ package ch.dissem.bitmessage.utils; * situations. */ public class Bytes { + public static final byte BYTE_0x80 = (byte) 0x80; + public static void inc(byte[] nonce) { for (int i = nonce.length - 1; i >= 0; i--) { nonce[i]++; @@ -82,11 +84,7 @@ public class Bytes { } private static boolean lt(byte a, byte b) { - if (a < 0) return b < 0 && a < b; - if (b < 0) return a >= 0 || a < b; - return a < b; - // This would be easier to understand, but is (slightly) slower: - // return (a & 0xff) < (b & 0xff); + return (a ^ BYTE_0x80) < (b ^ BYTE_0x80); } /** 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 47b0ee3..c15f397 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Decode.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Decode.java @@ -27,30 +27,29 @@ import static ch.dissem.bitmessage.utils.AccessCounter.inc; * https://bitmessage.org/wiki/Protocol_specification#Common_structures */ public class Decode { - public static byte[] shortVarBytes(InputStream stream, AccessCounter counter) throws IOException { - int length = uint16(stream, counter); - return bytes(stream, length, counter); + public static byte[] shortVarBytes(InputStream in, AccessCounter counter) throws IOException { + int length = uint16(in, counter); + return bytes(in, length, counter); } - public static byte[] varBytes(InputStream stream) throws IOException { - int length = (int) varInt(stream, null); - return bytes(stream, length, null); + public static byte[] varBytes(InputStream in) throws IOException { + return varBytes(in, null); } - public static byte[] varBytes(InputStream stream, AccessCounter counter) throws IOException { - int length = (int) varInt(stream, counter); - return bytes(stream, length, counter); + public static byte[] varBytes(InputStream in, AccessCounter counter) throws IOException { + int length = (int) varInt(in, counter); + return bytes(in, length, counter); } - public static byte[] bytes(InputStream stream, int count) throws IOException { - return bytes(stream, count, null); + public static byte[] bytes(InputStream in, int count) throws IOException { + return bytes(in, count, null); } - public static byte[] bytes(InputStream stream, int count, AccessCounter counter) throws IOException { + public static byte[] bytes(InputStream in, int count, AccessCounter counter) throws IOException { byte[] result = new byte[count]; int off = 0; while (off < count) { - int read = stream.read(result, off, count - off); + int read = in.read(result, off, count - off); if (read < 0) { throw new IOException("Unexpected end of stream, wanted to read " + count + " bytes but only got " + off); } @@ -60,83 +59,94 @@ public class Decode { return result; } - public static long[] varIntList(InputStream stream) throws IOException { - int length = (int) varInt(stream); + public static long[] varIntList(InputStream in) throws IOException { + int length = (int) varInt(in); long[] result = new long[length]; for (int i = 0; i < length; i++) { - result[i] = varInt(stream); + result[i] = varInt(in); } return result; } - public static long varInt(InputStream stream) throws IOException { - return varInt(stream, null); + public static long varInt(InputStream in) throws IOException { + return varInt(in, null); } - public static long varInt(InputStream stream, AccessCounter counter) throws IOException { - int first = stream.read(); + public static long varInt(InputStream in, AccessCounter counter) throws IOException { + int first = in.read(); inc(counter); switch (first) { case 0xfd: - return uint16(stream, counter); + return uint16(in, counter); case 0xfe: - return uint32(stream, counter); + return uint32(in, counter); case 0xff: - return int64(stream, counter); + return int64(in, counter); default: return first; } } - public static int uint8(InputStream stream) throws IOException { - return stream.read(); + public static int uint8(InputStream in) throws IOException { + return in.read(); } - public static int uint16(InputStream stream) throws IOException { - return uint16(stream, null); + public static int uint16(InputStream in) throws IOException { + return uint16(in, null); } - public static int uint16(InputStream stream, AccessCounter counter) throws IOException { + public static int uint16(InputStream in, AccessCounter counter) throws IOException { inc(counter, 2); - return stream.read() * 256 + stream.read(); + return in.read() << 8 | in.read(); } - public static long uint32(InputStream stream) throws IOException { - return uint32(stream, null); + public static long uint32(InputStream in) throws IOException { + return uint32(in, null); } - public static long uint32(InputStream stream, AccessCounter counter) throws IOException { + public static long uint32(InputStream in, AccessCounter counter) throws IOException { inc(counter, 4); - return stream.read() * 16777216L + stream.read() * 65536L + stream.read() * 256L + stream.read(); + return in.read() << 24 | in.read() << 16 | in.read() << 8 | in.read(); } - public static int int32(InputStream stream) throws IOException { - return int32(stream, null); + public static long uint32(ByteBuffer in) { + return u(in.get()) << 24 | u(in.get()) << 16 | u(in.get()) << 8 | u(in.get()); } - public static int int32(InputStream stream, AccessCounter counter) throws IOException { + public static int int32(InputStream in) throws IOException { + return int32(in, null); + } + + public static int int32(InputStream in, AccessCounter counter) throws IOException { inc(counter, 4); - return ByteBuffer.wrap(bytes(stream, 4)).getInt(); + return ByteBuffer.wrap(bytes(in, 4)).getInt(); } - public static long int64(InputStream stream) throws IOException { - return int64(stream, null); + public static long int64(InputStream in) throws IOException { + return int64(in, null); } - public static long int64(InputStream stream, AccessCounter counter) throws IOException { + public static long int64(InputStream in, AccessCounter counter) throws IOException { inc(counter, 8); - return ByteBuffer.wrap(bytes(stream, 8)).getLong(); + return ByteBuffer.wrap(bytes(in, 8)).getLong(); } - public static String varString(InputStream stream) throws IOException { - return varString(stream, null); + public static String varString(InputStream in) throws IOException { + return varString(in, null); } - public static String varString(InputStream stream, AccessCounter counter) throws IOException { - int length = (int) varInt(stream, counter); + 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 // otherwise it will get complicated, as we'll need to read UTF-8 char by char... - return new String(bytes(stream, length, counter), "utf-8"); + return new String(bytes(in, length, counter), "utf-8"); + } + + /** + * Returns the given byte as if it were unsigned. + */ + private static int u(byte b) { + return b & 0xFF; } } diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Encode.java b/core/src/main/java/ch/dissem/bitmessage/utils/Encode.java index 2cdc262..a60c027 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Encode.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Encode.java @@ -17,10 +17,13 @@ package ch.dissem.bitmessage.utils; import ch.dissem.bitmessage.entity.Streamable; +import ch.dissem.bitmessage.exception.ApplicationException; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.Buffer; import java.nio.ByteBuffer; import static ch.dissem.bitmessage.utils.AccessCounter.inc; @@ -37,36 +40,54 @@ public class Encode { } } + public static void varIntList(long[] values, ByteBuffer buffer) { + varInt(values.length, buffer); + for (long value : values) { + varInt(value, buffer); + } + } + public static void varInt(long value, OutputStream stream) throws IOException { varInt(value, stream, null); } - public static void varInt(long value, OutputStream stream, AccessCounter counter) throws IOException { + public static void varInt(long value, ByteBuffer buffer) { if (value < 0) { // This is due to the fact that Java doesn't really support unsigned values. // Please be aware that this might be an error due to a smaller negative value being cast to long. - // Normally, negative values shouldn't occur within the protocol, and I large enough longs - // to being recognized as negatives aren't realistic. - stream.write(0xff); - inc(counter); - int64(value, stream, counter); + // Normally, negative values shouldn't occur within the protocol, and longs large enough for being + // recognized as negatives aren't realistic. + buffer.put((byte) 0xff); + buffer.putLong(value); } else if (value < 0xfd) { - int8(value, stream, counter); + buffer.put((byte) value); } else if (value <= 0xffffL) { - stream.write(0xfd); - inc(counter); - int16(value, stream, counter); + buffer.put((byte) 0xfd); + buffer.putShort((short) value); } else if (value <= 0xffffffffL) { - stream.write(0xfe); - inc(counter); - int32(value, stream, counter); + buffer.put((byte) 0xfe); + buffer.putInt((int) value); } else { - stream.write(0xff); - inc(counter); - int64(value, stream, counter); + buffer.put((byte) 0xff); + buffer.putLong(value); } } + public static byte[] varInt(long value) { + ByteBuffer buffer = ByteBuffer.allocate(9); + varInt(value, buffer); + buffer.flip(); + return Bytes.truncate(buffer.array(), buffer.limit()); + } + + public static void varInt(long value, OutputStream stream, AccessCounter counter) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(9); + varInt(value, buffer); + buffer.flip(); + stream.write(buffer.array(), 0, buffer.limit()); + inc(counter, buffer.limit()); + } + public static void int8(long value, OutputStream stream) throws IOException { int8(value, stream, null); } @@ -81,10 +102,14 @@ public class Encode { } public static void int16(long value, OutputStream stream, AccessCounter counter) throws IOException { - stream.write(ByteBuffer.allocate(4).putInt((int) value).array(), 2, 2); + stream.write(ByteBuffer.allocate(2).putShort((short) value).array()); inc(counter, 2); } + public static void int16(long value, ByteBuffer buffer) { + buffer.putShort((short) value); + } + public static void int32(long value, OutputStream stream) throws IOException { int32(value, stream, null); } @@ -94,6 +119,10 @@ public class Encode { inc(counter, 4); } + public static void int32(long value, ByteBuffer buffer) { + buffer.putInt((int) value); + } + public static void int64(long value, OutputStream stream) throws IOException { int64(value, stream, null); } @@ -103,6 +132,10 @@ public class Encode { inc(counter, 8); } + public static void int64(long value, ByteBuffer buffer) { + buffer.putLong(value); + } + public static void varString(String value, OutputStream out) throws IOException { byte[] bytes = value.getBytes("utf-8"); // Technically, it says the length in characters, but I think this one might be correct. @@ -112,23 +145,44 @@ public class Encode { out.write(bytes); } + public static void varString(String value, ByteBuffer buffer) { + try { + byte[] bytes = value.getBytes("utf-8"); + // Technically, it says the length in characters, but I think this one might be correct. + // It doesn't really matter, as only ASCII characters are being used. + // see also Decode#varString() + buffer.put(varInt(bytes.length)); + buffer.put(bytes); + } catch (UnsupportedEncodingException e) { + throw new ApplicationException(e); + } + } + public static void varBytes(byte[] data, OutputStream out) throws IOException { varInt(data.length, out); out.write(data); } + public static void varBytes(byte[] data, ByteBuffer buffer) { + varInt(data.length, buffer); + buffer.put(data); + } + /** * Serializes a {@link Streamable} object and returns the byte array. * * @param streamable the object to be serialized * @return an array of bytes representing the given streamable object. - * @throws IOException if an I/O error occurs. */ - public static byte[] bytes(Streamable streamable) throws IOException { + public static byte[] bytes(Streamable streamable) { if (streamable == null) return null; ByteArrayOutputStream stream = new ByteArrayOutputStream(); - streamable.write(stream); + try { + streamable.write(stream); + } catch (IOException e) { + throw new ApplicationException(e); + } return stream.toByteArray(); } @@ -136,11 +190,14 @@ public class Encode { * @param streamable the object to be serialized * @param padding the result will be padded such that its length is a multiple of <em>padding</em> * @return the bytes of the given {@link Streamable} object, 0-padded such that the final length is x*padding. - * @throws IOException if an I/O error occurs. */ - public static byte[] bytes(Streamable streamable, int padding) throws IOException { + public static byte[] bytes(Streamable streamable, int padding) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); - streamable.write(stream); + try { + streamable.write(stream); + } catch (IOException e) { + throw new ApplicationException(e); + } int offset = padding - stream.size() % padding; int length = stream.size() + offset; byte[] result = new byte[length]; diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/Singleton.java b/core/src/main/java/ch/dissem/bitmessage/utils/Singleton.java index 0c7134b..2eeaa97 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Singleton.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Singleton.java @@ -19,20 +19,18 @@ package ch.dissem.bitmessage.utils; import ch.dissem.bitmessage.ports.Cryptography; /** - * Created by chris on 20.07.15. + * @author Christian Basler */ public class Singleton { private static Cryptography cryptography; public static void initialize(Cryptography cryptography) { synchronized (Singleton.class) { - if (Singleton.cryptography == null) { - Singleton.cryptography = cryptography; - } + Singleton.cryptography = cryptography; } } - public static Cryptography security() { + public static Cryptography cryptography() { return cryptography; } } diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/SqlStrings.java b/core/src/main/java/ch/dissem/bitmessage/utils/SqlStrings.java new file mode 100644 index 0000000..82d06e3 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/utils/SqlStrings.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016 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.payload.ObjectType; + +import static ch.dissem.bitmessage.utils.Strings.hex; + +public class SqlStrings { + public static StringBuilder join(long... objects) { + StringBuilder streamList = new StringBuilder(); + for (int i = 0; i < objects.length; i++) { + if (i > 0) streamList.append(", "); + streamList.append(objects[i]); + } + return streamList; + } + + public static StringBuilder join(byte[]... objects) { + StringBuilder streamList = new StringBuilder(); + for (int i = 0; i < objects.length; i++) { + if (i > 0) streamList.append(", "); + streamList.append(hex(objects[i])); + } + return streamList; + } + + public static StringBuilder join(ObjectType... types) { + StringBuilder streamList = new StringBuilder(); + for (int i = 0; i < types.length; i++) { + if (i > 0) streamList.append(", "); + streamList.append(types[i].getNumber()); + } + return streamList; + } + + public static StringBuilder join(Enum... types) { + StringBuilder streamList = new StringBuilder(); + for (int i = 0; i < types.length; i++) { + if (i > 0) streamList.append(", "); + streamList.append('\'').append(types[i].name()).append('\''); + } + return streamList; + } +} 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 5c1aae9..7c9e13f 100644 --- a/core/src/main/java/ch/dissem/bitmessage/utils/Strings.java +++ b/core/src/main/java/ch/dissem/bitmessage/utils/Strings.java @@ -16,8 +16,6 @@ package ch.dissem.bitmessage.utils; -import ch.dissem.bitmessage.entity.payload.ObjectType; - /** * Some utilities to handle strings. * TODO: Probably this should be split in a GUI related and an SQL related utility class. diff --git a/core/src/main/java/ch/dissem/bitmessage/utils/ThreadFactoryBuilder.java b/core/src/main/java/ch/dissem/bitmessage/utils/ThreadFactoryBuilder.java new file mode 100644 index 0000000..36cf5b9 --- /dev/null +++ b/core/src/main/java/ch/dissem/bitmessage/utils/ThreadFactoryBuilder.java @@ -0,0 +1,65 @@ +/* + * Copyright 2016 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 java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +public class ThreadFactoryBuilder { + private final String namePrefix; + private int prio = Thread.NORM_PRIORITY; + private boolean daemon = false; + + private ThreadFactoryBuilder(String pool) { + this.namePrefix = pool + "-thread-"; + } + + + public static ThreadFactoryBuilder pool(String name) { + return new ThreadFactoryBuilder(name); + } + + public ThreadFactoryBuilder lowPrio() { + prio = Thread.MIN_PRIORITY; + return this; + } + + public ThreadFactoryBuilder daemon() { + daemon = true; + return this; + } + + public ThreadFactory build() { + SecurityManager s = System.getSecurityManager(); + final ThreadGroup group = (s != null) ? s.getThreadGroup() : + Thread.currentThread().getThreadGroup(); + + return new ThreadFactory() { + private final AtomicInteger threadNumber = new AtomicInteger(1); + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(group, r, + namePrefix + threadNumber.getAndIncrement(), + 0); + t.setPriority(prio); + t.setDaemon(daemon); + return t; + } + }; + } +} diff --git a/core/src/main/resources/nodes.txt b/core/src/main/resources/nodes.txt index 9466a85..bce982e 100644 --- a/core/src/main/resources/nodes.txt +++ b/core/src/main/resources/nodes.txt @@ -5,4 +5,4 @@ bootstrap8080.bitmessage.org:8080 bootstrap8444.bitmessage.org:8444 [stream 2] -# none yet \ No newline at end of file +# none yet diff --git a/core/src/test/java/ch/dissem/bitmessage/BitmessageContextTest.java b/core/src/test/java/ch/dissem/bitmessage/BitmessageContextTest.java new file mode 100644 index 0000000..813ff20 --- /dev/null +++ b/core/src/test/java/ch/dissem/bitmessage/BitmessageContextTest.java @@ -0,0 +1,315 @@ +/* + * Copyright 2016 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; + +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.Plaintext.Type; +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.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; + +import java.util.*; + +import static ch.dissem.bitmessage.entity.payload.ObjectType.*; +import static ch.dissem.bitmessage.utils.MessageMatchers.object; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; +import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * @author Christian Basler + */ +public class BitmessageContextTest { + private BitmessageContext ctx; + private BitmessageContext.Listener listener; + + @Before + public void setUp() throws Exception { + 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<>(); + + @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 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 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"); + 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)); + } + + @Test + public void ensurePubkeyIsNotRequestedIfItExists() throws Exception { + ObjectMessage object = TestUtils.loadObjectMessage(2, "V2Pubkey.payload"); + Pubkey pubkey = (Pubkey) object.getPayload(); + BitmessageAddress contact = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"); + contact.setPubkey(pubkey); + + ctx.addContact(contact); + + verify(ctx.addresses(), times(1)).save(contact); + verify(ctx.internals().getProofOfWorkEngine(), never()) + .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") + )); + + 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)); + } + + @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") + )); + 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.internals().getProofOfWorkEngine(), never()) + .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); + } + + @Test + 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)); + + ctx.addSubscribtion(address); + + verify(ctx.addresses(), atLeastOnce()).save(address); + assertThat(address.isSubscribed(), is(true)); + verify(ctx.internals().getInventory()).getObjects(eq(address.getStream()), anyLong(), any(ObjectType.class)); + verify(listener).receive(any(Plaintext.class)); + } + + @Test + public void ensureIdentityIsCreated() { + assertThat(ctx.createIdentity(false), notNullValue()); + } + + @Test + public void ensureMessageIsSent() throws Exception { + ctx.send(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), TestUtils.loadContact(), + "Subject", "Message"); + assertEquals(2, ctx.internals().getProofOfWorkRepository().getItems().size()); + verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce()) + .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"); + verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce()) + .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"); + } + + @Test + public void ensureBroadcastIsSent() throws Exception { + ctx.broadcast(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"), + "Subject", "Message"); + verify(ctx.internals().getProofOfWorkRepository(), timeout(10000).atLeastOnce()) + .putObject(object(BROADCAST), eq(1000L), eq(1000L)); + verify(ctx.internals().getProofOfWorkEngine()) + .calculateNonce(any(byte[].class), any(byte[].class), any(ProofOfWorkEngine.Callback.class)); + verify(ctx.messages(), timeout(10000).atLeastOnce()) + .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(); + ctx.send(msg); + } + + @Test + public void ensureChanIsJoined() { + String chanAddress = "BM-2cW67GEKkHGonXKZLCzouLLxnLym3azS8r"; + BitmessageAddress chan = ctx.joinChan("general", chanAddress); + assertNotNull(chan); + assertEquals(chan.getAddress(), chanAddress); + assertTrue(chan.isChan()); + } + + @Test + public void ensureDeterministicAddressesAreCreated() { + final int expected_size = 8; + List<BitmessageAddress> addresses = ctx.createDeterministicAddresses("test", expected_size, 4, 1, false); + assertEquals(expected_size, addresses.size()); + Set<String> expected = new HashSet<>(expected_size); + expected.add("BM-2cWFkyuXXFw6d393RGnin2RpSXj8wxtt6F"); + expected.add("BM-2cX8TF9vuQZEWvT7UrEeq1HN9dgiSUPLEN"); + expected.add("BM-2cUzX8f9CKUU7L8NeB8GExZvf54PrcXq1S"); + expected.add("BM-2cU7MAoQd7KE8SPF7AKFPpoEZKjk86KRqE"); + expected.add("BM-2cVm8ByVBacc2DVhdTNs6rmy5ZQK6DUsrt"); + expected.add("BM-2cW2af1vB6kWon2WkygDHqGwfcpfAFm2Jk"); + expected.add("BM-2cWdWD7UtUN4gWChgNX9pvyvNPjUZvU8BT"); + expected.add("BM-2cXkYgYcUrv4fGxSHzyEScW955Cc8sDteo"); + for (BitmessageAddress a : addresses) { + assertTrue(expected.contains(a.getAddress())); + expected.remove(a.getAddress()); + } + } + + @Test + public void ensureShortDeterministicAddressesAreCreated() { + final int expected_size = 1; + List<BitmessageAddress> addresses = ctx.createDeterministicAddresses("test", expected_size, 4, 1, true); + assertEquals(expected_size, addresses.size()); + Set<String> expected = new HashSet<>(expected_size); + expected.add("BM-NBGyBAEp6VnBkFWKpzUSgxuTqVdWPi78"); + for (BitmessageAddress a : addresses) { + assertTrue(expected.contains(a.getAddress())); + expected.remove(a.getAddress()); + } + } + + @Test + public void ensureChanIsCreated() { + BitmessageAddress chan = ctx.createChan("test"); + assertNotNull(chan); + assertEquals(chan.getVersion(), Pubkey.LATEST_VERSION); + assertTrue(chan.isChan()); + } + + @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(); + 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); + ctx.resendUnacknowledgedMessages(); + verify(ctx.labeler(), timeout(1000).times(1)).markAsSent(eq(plaintext)); + } +} diff --git a/core/src/test/java/ch/dissem/bitmessage/DefaultMessageListenerTest.java b/core/src/test/java/ch/dissem/bitmessage/DefaultMessageListenerTest.java new file mode 100644 index 0000000..32a20e4 --- /dev/null +++ b/core/src/test/java/ch/dissem/bitmessage/DefaultMessageListenerTest.java @@ -0,0 +1,157 @@ +/* + * Copyright 2016 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; + +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.payload.Broadcast; +import ch.dissem.bitmessage.entity.payload.GetPubkey; +import ch.dissem.bitmessage.entity.payload.Msg; +import ch.dissem.bitmessage.factory.Factory; +import ch.dissem.bitmessage.ports.*; +import ch.dissem.bitmessage.utils.Singleton; +import ch.dissem.bitmessage.utils.TestBase; +import ch.dissem.bitmessage.utils.TestUtils; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Collections; + +import static ch.dissem.bitmessage.entity.Plaintext.Status.PUBKEY_REQUESTED; +import static ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST; +import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; +import static ch.dissem.bitmessage.utils.MessageMatchers.plaintext; +import static org.mockito.Mockito.*; + +/** + * @author Christian Basler + */ +public class DefaultMessageListenerTest extends TestBase { + @Mock + private AddressRepository addressRepo; + @Mock + private MessageRepository messageRepo; + @Mock + private Inventory inventory; + @Mock + private NetworkHandler networkHandler; + + private InternalContext ctx; + private DefaultMessageListener listener; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + ctx = mock(InternalContext.class); + Singleton.initialize(new BouncyCryptography()); + when(ctx.getAddressRepository()).thenReturn(addressRepo); + when(ctx.getMessageRepository()).thenReturn(messageRepo); + when(ctx.getInventory()).thenReturn(inventory); + when(ctx.getNetworkHandler()).thenReturn(networkHandler); + when(ctx.getLabeler()).thenReturn(mock(Labeler.class)); + + listener = new DefaultMessageListener(mock(Labeler.class), mock(BitmessageContext.Listener.class)); + when(ctx.getNetworkListener()).thenReturn(listener); + listener.setContext(ctx); + } + + @Test + public void ensurePubkeyIsSentOnRequest() throws Exception { + BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); + when(addressRepo.findIdentity(any(byte[].class))) + .thenReturn(identity); + listener.receive(new ObjectMessage.Builder() + .stream(2) + .payload(new GetPubkey(new BitmessageAddress("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"))) + .build()); + verify(ctx).sendPubkey(eq(identity), eq(2L)); + } + + @Test + public void ensureIncomingPubkeyIsAddedToContact() throws Exception { + BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); + BitmessageAddress contact = new BitmessageAddress(identity.getAddress()); + when(addressRepo.findContact(any(byte[].class))) + .thenReturn(contact); + when(messageRepo.findMessages(eq(PUBKEY_REQUESTED), eq(contact))) + .thenReturn(Collections.singletonList( + new Plaintext.Builder(MSG).from(identity).to(contact).message("S", "T").build() + )); + + ObjectMessage objectMessage = new ObjectMessage.Builder() + .stream(2) + .payload(identity.getPubkey()) + .build(); + objectMessage.sign(identity.getPrivateKey()); + objectMessage.encrypt(Singleton.cryptography().createPublicKey(identity.getPublicDecryptionKey())); + listener.receive(objectMessage); + + verify(addressRepo).save(any(BitmessageAddress.class)); + } + + @Test + public void ensureIncomingMessageIsSaved() throws Exception { + BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); + BitmessageAddress contact = new BitmessageAddress(identity.getAddress()); + contact.setPubkey(identity.getPubkey()); + + when(addressRepo.getIdentities()).thenReturn(Collections.singletonList(identity)); + + ObjectMessage objectMessage = new ObjectMessage.Builder() + .stream(2) + .payload(new Msg(new Plaintext.Builder(MSG) + .from(identity) + .to(contact) + .message("S", "T") + .build())) + .nonce(new byte[8]) + .build(); + objectMessage.sign(identity.getPrivateKey()); + objectMessage.encrypt(identity.getPubkey()); + + listener.receive(objectMessage); + + verify(messageRepo, atLeastOnce()).save(plaintext(MSG)); + } + + @Test + public void ensureIncomingBroadcastIsSaved() throws Exception { + BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); + + when(addressRepo.getSubscriptions(anyLong())).thenReturn(Collections.singletonList(identity)); + + Broadcast broadcast = Factory.getBroadcast(new Plaintext.Builder(BROADCAST) + .from(identity) + .message("S", "T") + .build()); + ObjectMessage objectMessage = new ObjectMessage.Builder() + .stream(2) + .payload(broadcast) + .nonce(new byte[8]) + .build(); + objectMessage.sign(identity.getPrivateKey()); + broadcast.encrypt(); + + listener.receive(objectMessage); + + verify(messageRepo, atLeastOnce()).save(plaintext(BROADCAST)); + } +} diff --git a/core/src/test/java/ch/dissem/bitmessage/EncryptionTest.java b/core/src/test/java/ch/dissem/bitmessage/EncryptionTest.java index 9a24842..0a6ee25 100644 --- a/core/src/test/java/ch/dissem/bitmessage/EncryptionTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/EncryptionTest.java @@ -30,19 +30,19 @@ import org.junit.Test; import java.io.IOException; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; public class EncryptionTest extends TestBase { @Test public void ensureDecryptedDataIsSameAsBeforeEncryption() throws IOException, DecryptionFailedException { - GenericPayload before = new GenericPayload(0, 1, security().randomBytes(100)); + GenericPayload before = new GenericPayload(0, 1, cryptography().randomBytes(100)); PrivateKey privateKey = new PrivateKey(false, 1, 1000, 1000); CryptoBox cryptoBox = new CryptoBox(before, privateKey.getPubkey().getEncryptionKey()); - GenericPayload after = GenericPayload.read(0, cryptoBox.decrypt(privateKey.getPrivateEncryptionKey()), 1, 100); + GenericPayload after = GenericPayload.read(0, 1, cryptoBox.decrypt(privateKey.getPrivateEncryptionKey()), 100); assertEquals(before, after); } diff --git a/core/src/test/java/ch/dissem/bitmessage/ProofOfWorkServiceTest.java b/core/src/test/java/ch/dissem/bitmessage/ProofOfWorkServiceTest.java new file mode 100644 index 0000000..039e7ad --- /dev/null +++ b/core/src/test/java/ch/dissem/bitmessage/ProofOfWorkServiceTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2016 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; + +import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.payload.Msg; +import ch.dissem.bitmessage.ports.*; +import ch.dissem.bitmessage.utils.Singleton; +import ch.dissem.bitmessage.utils.TestUtils; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; + +import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.*; + +/** + * @author Christian Basler + */ +public class ProofOfWorkServiceTest { + private ProofOfWorkService proofOfWorkService; + + private Cryptography cryptography; + @Mock + private InternalContext ctx; + @Mock + private ProofOfWorkRepository proofOfWorkRepo; + @Mock + private Inventory inventory; + @Mock + private NetworkHandler networkHandler; + @Mock + private MessageRepository messageRepo; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + cryptography = spy(new BouncyCryptography()); + Singleton.initialize(cryptography); + + ctx = mock(InternalContext.class); + when(ctx.getProofOfWorkRepository()).thenReturn(proofOfWorkRepo); + when(ctx.getInventory()).thenReturn(inventory); + when(ctx.getNetworkHandler()).thenReturn(networkHandler); + when(ctx.getMessageRepository()).thenReturn(messageRepo); + when(ctx.getLabeler()).thenReturn(mock(Labeler.class)); + when(ctx.getNetworkListener()).thenReturn(mock(NetworkHandler.MessageListener.class)); + + proofOfWorkService = new ProofOfWorkService(); + proofOfWorkService.setContext(ctx); + } + + @Test + public void ensureMissingProofOfWorkIsDone() { + when(proofOfWorkRepo.getItems()).thenReturn(Arrays.asList(new byte[64])); + when(proofOfWorkRepo.getItem(any(byte[].class))).thenReturn(new ProofOfWorkRepository.Item(null, 1001, 1002)); + doNothing().when(cryptography).doProofOfWork(any(ObjectMessage.class), anyLong(), anyLong(), any(ProofOfWorkEngine.Callback.class)); + + proofOfWorkService.doMissingProofOfWork(10); + + verify(cryptography, timeout(1000)).doProofOfWork((ObjectMessage) isNull(), eq(1001L), eq(1002L), + any(ProofOfWorkEngine.Callback.class)); + } + + @Test + public void ensureCalculatedNonceIsStored() throws Exception { + BitmessageAddress identity = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); + BitmessageAddress address = TestUtils.loadContact(); + Plaintext plaintext = new Plaintext.Builder(MSG).from(identity).to(address).message("", "").build(); + ObjectMessage object = new ObjectMessage.Builder() + .payload(new Msg(plaintext)) + .build(); + object.sign(identity.getPrivateKey()); + object.encrypt(address.getPubkey()); + byte[] initialHash = new byte[64]; + byte[] nonce = new byte[]{1, 2, 3, 4, 5, 6, 7, 8}; + + when(proofOfWorkRepo.getItem(initialHash)).thenReturn(new ProofOfWorkRepository.Item(object, 1001, 1002)); + when(messageRepo.getMessage(initialHash)).thenReturn(plaintext); + + proofOfWorkService.onNonceCalculated(initialHash, nonce); + + verify(proofOfWorkRepo).removeObject(eq(initialHash)); + verify(inventory).storeObject(eq(object)); + verify(networkHandler).offer(eq(object.getInventoryVector())); + assertThat(plaintext.getInventoryVector(), equalTo(object.getInventoryVector())); + } +} diff --git a/core/src/test/java/ch/dissem/bitmessage/SignatureTest.java b/core/src/test/java/ch/dissem/bitmessage/SignatureTest.java index 71b7d2a..3566a5f 100644 --- a/core/src/test/java/ch/dissem/bitmessage/SignatureTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/SignatureTest.java @@ -22,7 +22,6 @@ import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.payload.Msg; import ch.dissem.bitmessage.entity.payload.ObjectType; import ch.dissem.bitmessage.entity.payload.Pubkey; -import ch.dissem.bitmessage.entity.payload.V4Pubkey; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.utils.TestBase; @@ -30,7 +29,6 @@ import ch.dissem.bitmessage.utils.TestUtils; import org.junit.Test; import java.io.IOException; -import java.util.Date; import static org.junit.Assert.*; diff --git a/core/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java b/core/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java index e1fcc7f..fced84f 100644 --- a/core/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/entity/BitmessageAddressTest.java @@ -27,10 +27,18 @@ import java.io.IOException; import java.util.Arrays; import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.INCLUDE_DESTINATION; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; import static org.junit.Assert.*; -public class BitmessageAddressTest { +public class BitmessageAddressTest extends TestBase { + @Test + public void ensureFeatureFlagIsCalculatedCorrectly() { + assertEquals(1, Pubkey.Feature.bitfield(DOES_ACK)); + assertEquals(2, Pubkey.Feature.bitfield(INCLUDE_DESTINATION)); + assertEquals(3, Pubkey.Feature.bitfield(DOES_ACK, INCLUDE_DESTINATION)); + } + @Test public void ensureBase58DecodesCorrectly() { assertHexEquals("800C28FCA386C7A227600B2FE50B7CAE11EC86D3BF1FBE471BE89827E19D72AA1D507A5B8D", @@ -55,44 +63,59 @@ public class BitmessageAddressTest { } @Test - public void testCreateAddress() { + public void ensureIdentityCanBeCreated() { BitmessageAddress address = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000, DOES_ACK)); assertNotNull(address.getPubkey()); + assertTrue(address.has(DOES_ACK)); } @Test - public void testV2PubkeyImport() throws IOException { + public void ensureV2PubkeyCanBeImported() throws IOException { ObjectMessage object = TestUtils.loadObjectMessage(2, "V2Pubkey.payload"); Pubkey pubkey = (Pubkey) object.getPayload(); BitmessageAddress address = new BitmessageAddress("BM-opWQhvk9xtMFvQA2Kvetedpk8LkbraWHT"); - address.setPubkey(pubkey); + try { + address.setPubkey(pubkey); + } catch (Exception e) { + fail(e.getMessage()); + } } @Test - public void testV3PubkeyImport() throws IOException { + public void ensureV3PubkeyCanBeImported() throws IOException { BitmessageAddress address = new BitmessageAddress("BM-2D9Vc5rFxxR5vTi53T9gkLfemViHRMVLQZ"); assertArrayEquals(Bytes.fromHex("007402be6e76c3cb87caa946d0c003a3d4d8e1d5"), address.getRipe()); ObjectMessage object = TestUtils.loadObjectMessage(3, "V3Pubkey.payload"); Pubkey pubkey = (Pubkey) object.getPayload(); assertTrue(object.isSignatureValid(pubkey)); - address.setPubkey(pubkey); + try { + address.setPubkey(pubkey); + } catch (Exception e) { + fail(e.getMessage()); + } assertArrayEquals(Bytes.fromHex("007402be6e76c3cb87caa946d0c003a3d4d8e1d5"), pubkey.getRipe()); + assertTrue(address.has(DOES_ACK)); } @Test - public void testV4PubkeyImport() throws IOException, DecryptionFailedException { + public void ensureV4PubkeyCanBeImported() throws IOException, DecryptionFailedException { BitmessageAddress address = new BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h"); ObjectMessage object = TestUtils.loadObjectMessage(4, "V4Pubkey.payload"); object.decrypt(address.getPublicDecryptionKey()); V4Pubkey pubkey = (V4Pubkey) object.getPayload(); assertTrue(object.isSignatureValid(pubkey)); - address.setPubkey(pubkey); + try { + address.setPubkey(pubkey); + } catch (Exception e) { + fail(e.getMessage()); + } + assertTrue(address.has(DOES_ACK)); } @Test - public void testV3AddressImport() throws IOException { + public void ensureV3IdentityCanBeImported() throws IOException { String address_string = "BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn"; assertEquals(3, new BitmessageAddress(address_string).getVersion()); assertEquals(1, new BitmessageAddress(address_string).getStream()); @@ -103,14 +126,22 @@ public class BitmessageAddressTest { System.out.println("\n\n" + Strings.hex(privsigningkey) + "\n\n"); BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey, - security().createPubkey(3, 1, privsigningkey, privencryptionkey, 320, 14000))); + cryptography().createPubkey(3, 1, privsigningkey, privencryptionkey, 320, 14000))); assertEquals(address_string, address.getAddress()); } @Test - public void testGetSecret() throws IOException { - assertHexEquals("0C28FCA386C7A227600B2FE50B7CAE11EC86D3BF1FBE471BE89827E19D72AA1D", - getSecret("5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ")); + public void ensureV4IdentityCanBeImported() throws IOException { + assertEquals(4, new BitmessageAddress("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke").getVersion()); + byte[] privsigningkey = getSecret("5KMWqfCyJZGFgW6QrnPJ6L9Gatz25B51y7ErgqNr1nXUVbtZbdU"); + byte[] privencryptionkey = getSecret("5JXXWEuhHQEPk414SzEZk1PHDRi8kCuZd895J7EnKeQSahJPxGz"); + BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey, + cryptography().createPubkey(4, 1, privsigningkey, privencryptionkey, 320, 14000))); + assertEquals("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke", address.getAddress()); + } + + private void assertHexEquals(String hex, byte[] bytes) { + assertEquals(hex.toLowerCase(), Strings.hex(bytes).toString().toLowerCase()); } private byte[] getSecret(String walletImportFormat) throws IOException { @@ -120,24 +151,10 @@ public class BitmessageAddressTest { if (bytes.length != 37) throw new IOException("Unknown format: 37 bytes expected, but secret " + walletImportFormat + " was " + bytes.length + " long"); - byte[] hash = security().doubleSha256(bytes, 33); + byte[] hash = cryptography().doubleSha256(bytes, 33); for (int i = 0; i < 4; i++) { if (hash[i] != bytes[33 + i]) throw new IOException("Hash check failed for secret " + walletImportFormat); } return Arrays.copyOfRange(bytes, 1, 33); } - - @Test - public void testV4AddressImport() throws IOException { - assertEquals(4, new BitmessageAddress("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke").getVersion()); - byte[] privsigningkey = getSecret("5KMWqfCyJZGFgW6QrnPJ6L9Gatz25B51y7ErgqNr1nXUVbtZbdU"); - byte[] privencryptionkey = getSecret("5JXXWEuhHQEPk414SzEZk1PHDRi8kCuZd895J7EnKeQSahJPxGz"); - BitmessageAddress address = new BitmessageAddress(new PrivateKey(privsigningkey, privencryptionkey, - security().createPubkey(4, 1, privsigningkey, privencryptionkey, 320, 14000))); - assertEquals("BM-2cV5f9EpzaYARxtoruSpa6pDoucSf9ZNke", address.getAddress()); - } - - private void assertHexEquals(String hex, byte[] bytes) { - assertEquals(hex.toLowerCase(), Strings.hex(bytes).toString().toLowerCase()); - } } 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 1bcb8e7..5405f23 100644 --- a/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/entity/SerializationTest.java @@ -30,7 +30,7 @@ import java.util.ArrayList; import java.util.Collections; import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; import static org.junit.Assert.*; public class SerializationTest extends TestBase { @@ -82,7 +82,7 @@ public class SerializationTest extends TestBase { .from(TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8")) .to(TestUtils.loadContact()) .message("Subject", "Message") - .ack("ack".getBytes()) + .ackData("ackMessage".getBytes()) .signature(new byte[0]) .build(); ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -98,11 +98,37 @@ public class SerializationTest extends TestBase { assertEquals(p1, p2); } + @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(); + ObjectMessage ackMessage1 = p1.getAckMessage(); + assertNotNull(ackMessage1); + + 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); + assertEquals(ackMessage1, p2.getAckMessage()); + } + @Test public void ensureNetworkMessageIsSerializedAndDeserializedCorrectly() throws Exception { ArrayList<InventoryVector> ivs = new ArrayList<>(50000); for (int i = 0; i < 50000; i++) { - ivs.add(new InventoryVector(security().randomBytes(32))); + ivs.add(new InventoryVector(cryptography().randomBytes(32))); } Inv inv = new Inv.Builder().inventory(ivs).build(); @@ -111,6 +137,7 @@ public class SerializationTest extends TestBase { before.write(out); NetworkMessage after = Factory.getNetworkMessage(3, new ByteArrayInputStream(out.toByteArray())); + assertNotNull(after); Inv invAfter = (Inv) after.getPayload(); assertEquals(ivs, invAfter.getInventory()); } diff --git a/core/src/test/java/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.java b/core/src/test/java/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.java index 1ed4aac..c2efb1f 100644 --- a/core/src/test/java/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/ports/ProofOfWorkEngineTest.java @@ -21,7 +21,7 @@ import ch.dissem.bitmessage.utils.CallbackWaiter; import ch.dissem.bitmessage.utils.TestBase; import org.junit.Test; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; import static org.junit.Assert.assertTrue; public class ProofOfWorkEngineTest extends TestBase { @@ -36,7 +36,7 @@ public class ProofOfWorkEngineTest extends TestBase { } private void testPOW(ProofOfWorkEngine engine) throws InterruptedException { - byte[] initialHash = security().sha512(new byte[]{1, 3, 6, 4}); + byte[] initialHash = cryptography().sha512(new byte[]{1, 3, 6, 4}); byte[] target = {0, 0, 0, -1, -1, -1, -1, -1}; final CallbackWaiter<byte[]> waiter1 = new CallbackWaiter<>(); @@ -49,10 +49,10 @@ public class ProofOfWorkEngineTest extends TestBase { }); byte[] nonce = waiter1.waitForValue(); System.out.println("Calculating nonce took " + waiter1.getTime() + "ms"); - assertTrue(Bytes.lt(security().doubleSha512(nonce, initialHash), target, 8)); + assertTrue(Bytes.lt(cryptography().doubleSha512(nonce, initialHash), target, 8)); // Let's add a second (shorter) run to find possible multi threading issues - byte[] initialHash2 = security().sha512(new byte[]{1, 3, 6, 5}); + byte[] initialHash2 = cryptography().sha512(new byte[]{1, 3, 6, 5}); byte[] target2 = {0, 0, -1, -1, -1, -1, -1, -1}; final CallbackWaiter<byte[]> waiter2 = new CallbackWaiter<>(); @@ -65,7 +65,7 @@ public class ProofOfWorkEngineTest extends TestBase { }); byte[] nonce2 = waiter2.waitForValue(); System.out.println("Calculating nonce took " + waiter2.getTime() + "ms"); - assertTrue(Bytes.lt(security().doubleSha512(nonce2, initialHash2), target2, 8)); + assertTrue(Bytes.lt(cryptography().doubleSha512(nonce2, initialHash2), target2, 8)); assertTrue("Second nonce must be quicker to find", waiter1.getTime() > waiter2.getTime()); } diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/EncodeTest.java b/core/src/test/java/ch/dissem/bitmessage/utils/EncodeTest.java index aaba5bf..23d602c 100644 --- a/core/src/test/java/ch/dissem/bitmessage/utils/EncodeTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/utils/EncodeTest.java @@ -57,7 +57,7 @@ public class EncodeTest { checkBytes(stream, 4, 3, 2, 1); stream = new ByteArrayOutputStream(); - Encode.int32(3355443201l, stream); + Encode.int32(3355443201L, stream); checkBytes(stream, 200, 0, 0, 1); } diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/MessageMatchers.java b/core/src/test/java/ch/dissem/bitmessage/utils/MessageMatchers.java new file mode 100644 index 0000000..e423d02 --- /dev/null +++ b/core/src/test/java/ch/dissem/bitmessage/utils/MessageMatchers.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016 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.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; + +/** + * @author Christian Basler + */ +public class MessageMatchers { + public static Plaintext plaintext(final Plaintext.Type type) { + return Matchers.argThat(new BaseMatcher<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 static ObjectMessage object(final ObjectType type) { + return Matchers.argThat(new BaseMatcher<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); + } + }); + } +} diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcHelperTest.java b/core/src/test/java/ch/dissem/bitmessage/utils/SqlStringsTest.java similarity index 81% rename from repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcHelperTest.java rename to core/src/test/java/ch/dissem/bitmessage/utils/SqlStringsTest.java index ee3a580..2aa1113 100644 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcHelperTest.java +++ b/core/src/test/java/ch/dissem/bitmessage/utils/SqlStringsTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 Christian Basler + * Copyright 2016 Christian Basler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,16 @@ * limitations under the License. */ -package ch.dissem.bitmessage.repository; +package ch.dissem.bitmessage.utils; import org.junit.Test; import static org.junit.Assert.assertEquals; -public class JdbcHelperTest { +public class SqlStringsTest { @Test public void ensureJoinWorksWithLongArray() { long[] test = {1L, 2L}; - assertEquals("1, 2", JdbcHelper.join(test).toString()); + assertEquals("1, 2", SqlStrings.join(test).toString()); } } diff --git a/core/src/test/java/ch/dissem/bitmessage/utils/TestBase.java b/core/src/test/java/ch/dissem/bitmessage/utils/TestBase.java index e757d91..4c73fe3 100644 --- a/core/src/test/java/ch/dissem/bitmessage/utils/TestBase.java +++ b/core/src/test/java/ch/dissem/bitmessage/utils/TestBase.java @@ -17,12 +17,14 @@ package ch.dissem.bitmessage.utils; import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; +import org.junit.BeforeClass; /** - * Created by chris on 20.07.15. + * @author Christian Basler */ public class TestBase { - static { + @BeforeClass + public static void setUpClass() { Singleton.initialize(new BouncyCryptography()); } } 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 770039d..21f85ff 100644 --- a/core/src/test/java/ch/dissem/bitmessage/utils/TestUtils.java +++ b/core/src/test/java/ch/dissem/bitmessage/utils/TestUtils.java @@ -72,7 +72,7 @@ public class TestUtils { public static BitmessageAddress loadContact() throws IOException, DecryptionFailedException { BitmessageAddress address = new BitmessageAddress("BM-2cXxfcSetKnbHJX2Y85rSkaVpsdNUZ5q9h"); - ObjectMessage object = TestUtils.loadObjectMessage(4, "V4Pubkey.payload"); + ObjectMessage object = TestUtils.loadObjectMessage(3, "V4Pubkey.payload"); object.decrypt(address.getPublicDecryptionKey()); address.setPubkey((V4Pubkey) object.getPayload()); return address; diff --git a/cryptography-bc/build.gradle b/cryptography-bc/build.gradle index c09b0db..0b87c17 100644 --- a/cryptography-bc/build.gradle +++ b/cryptography-bc/build.gradle @@ -13,6 +13,6 @@ uploadArchives { dependencies { compile project(':core') compile 'org.bouncycastle:bcprov-jdk15on:1.52' - testCompile 'junit:junit:4.11' + testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' } 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 28be67a..377135c 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 @@ -18,6 +18,7 @@ package ch.dissem.bitmessage.cryptography.bc; import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.ports.AbstractCryptography; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.BufferedBlockCipher; @@ -37,6 +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; @@ -49,19 +51,23 @@ 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"; + private static final String PROVIDER = "BC"; static { java.security.Security.addProvider(new BouncyCastleProvider()); } public BouncyCryptography() { - super("BC"); + super(PROVIDER); } @Override public byte[] crypt(boolean encrypt, byte[] data, byte[] key_e, byte[] initializationVector) { - BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()), new PKCS7Padding()); - + BufferedBlockCipher cipher = new PaddedBufferedBlockCipher( + new CBCBlockCipher(new AESEngine()), + new PKCS7Padding() + ); CipherParameters params = new ParametersWithIV(new KeyParameter(key_e), initializationVector); cipher.init(encrypt, params); @@ -103,14 +109,14 @@ public class BouncyCryptography extends AbstractCryptography { ECPoint Q = keyToPoint(pubkey.getSigningKey()); KeySpec keySpec = new ECPublicKeySpec(Q, spec); - PublicKey publicKey = KeyFactory.getInstance("ECDSA", "BC").generatePublic(keySpec); + PublicKey publicKey = KeyFactory.getInstance(ALGORITHM_ECDSA, PROVIDER).generatePublic(keySpec); - Signature sig = Signature.getInstance("ECDSA", "BC"); + Signature sig = Signature.getInstance(ALGORITHM_ECDSA, PROVIDER); sig.initVerify(publicKey); sig.update(data); return sig.verify(signature); - } catch (Exception e) { - throw new RuntimeException(e); + } catch (GeneralSecurityException e) { + throw new ApplicationException(e); } } @@ -127,14 +133,15 @@ public class BouncyCryptography extends AbstractCryptography { BigInteger d = keyToBigInt(privateKey.getPrivateSigningKey()); KeySpec keySpec = new ECPrivateKeySpec(d, spec); - java.security.PrivateKey privKey = KeyFactory.getInstance("ECDSA", "BC").generatePrivate(keySpec); + java.security.PrivateKey privKey = KeyFactory.getInstance(ALGORITHM_ECDSA, PROVIDER) + .generatePrivate(keySpec); - Signature sig = Signature.getInstance("ECDSA", "BC"); + Signature sig = Signature.getInstance(ALGORITHM_ECDSA, PROVIDER); sig.initSign(privKey); sig.update(data); return sig.sign(); - } catch (Exception e) { - throw new RuntimeException(e); + } catch (GeneralSecurityException e) { + throw new ApplicationException(e); } } diff --git a/cryptography-bc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java b/cryptography-bc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java index d4ab2ad..b0937d3 100644 --- a/cryptography-bc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java +++ b/cryptography-bc/src/test/java/ch/dissem/bitmessage/security/CryptographyTest.java @@ -4,19 +4,24 @@ import ch.dissem.bitmessage.InternalContext; import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.payload.GenericPayload; +import ch.dissem.bitmessage.entity.valueobject.PrivateKey; +import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException; import ch.dissem.bitmessage.ports.MultiThreadedPOWEngine; import ch.dissem.bitmessage.ports.ProofOfWorkEngine; import ch.dissem.bitmessage.utils.CallbackWaiter; import ch.dissem.bitmessage.utils.Singleton; import ch.dissem.bitmessage.utils.UnixTime; +import org.junit.BeforeClass; import org.junit.Test; import javax.xml.bind.DatatypeConverter; import java.io.ByteArrayInputStream; import java.io.IOException; +import static ch.dissem.bitmessage.utils.UnixTime.DAY; import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; -import static org.junit.Assert.assertArrayEquals; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -33,50 +38,51 @@ public class CryptographyTest { public static final byte[] TEST_RIPEMD160 = DatatypeConverter.parseHexBinary("" + "cd566972b5e50104011a92b59fa8e0b1234851ae"); - private static BouncyCryptography security; + private static BouncyCryptography crypto; - public CryptographyTest() { - security = new BouncyCryptography(); - Singleton.initialize(security); + @BeforeClass + public static void setUp() { + crypto = new BouncyCryptography(); + Singleton.initialize(crypto); InternalContext ctx = mock(InternalContext.class); when(ctx.getProofOfWorkEngine()).thenReturn(new MultiThreadedPOWEngine()); - security.setContext(ctx); + crypto.setContext(ctx); } @Test public void testRipemd160() { - assertArrayEquals(TEST_RIPEMD160, security.ripemd160(TEST_VALUE)); + assertArrayEquals(TEST_RIPEMD160, crypto.ripemd160(TEST_VALUE)); } @Test public void testSha1() { - assertArrayEquals(TEST_SHA1, security.sha1(TEST_VALUE)); + assertArrayEquals(TEST_SHA1, crypto.sha1(TEST_VALUE)); } @Test public void testSha512() { - assertArrayEquals(TEST_SHA512, security.sha512(TEST_VALUE)); + assertArrayEquals(TEST_SHA512, crypto.sha512(TEST_VALUE)); } @Test public void testChaining() { - assertArrayEquals(TEST_SHA512, security.sha512("test".getBytes(), "string".getBytes())); + assertArrayEquals(TEST_SHA512, crypto.sha512("test".getBytes(), "string".getBytes())); } @Test - public void testDoubleHash() { - assertArrayEquals(security.sha512(TEST_SHA512), security.doubleSha512(TEST_VALUE)); + public void ensureDoubleHashYieldsSameResultAsHashOfHash() { + assertArrayEquals(crypto.sha512(TEST_SHA512), crypto.doubleSha512(TEST_VALUE)); } @Test(expected = IOException.class) - public void testProofOfWorkFails() throws IOException { + public void ensureExceptionForInsufficientProofOfWork() throws IOException { ObjectMessage objectMessage = new ObjectMessage.Builder() .nonce(new byte[8]) - .expiresTime(UnixTime.now(+2 * MINUTE)) + .expiresTime(UnixTime.now(+28 * DAY)) .objectType(0) - .payload(GenericPayload.read(0, new ByteArrayInputStream(new byte[0]), 1, 0)) + .payload(GenericPayload.read(0, 1, new ByteArrayInputStream(new byte[0]), 0)) .build(); - security.checkProofOfWork(objectMessage, 1000, 1000); + crypto.checkProofOfWork(objectMessage, 1000, 1000); } @Test @@ -85,10 +91,10 @@ public class CryptographyTest { .nonce(new byte[8]) .expiresTime(UnixTime.now(+2 * MINUTE)) .objectType(0) - .payload(GenericPayload.read(0, new ByteArrayInputStream(new byte[0]), 1, 0)) + .payload(GenericPayload.read(0, 1, new ByteArrayInputStream(new byte[0]), 0)) .build(); final CallbackWaiter<byte[]> waiter = new CallbackWaiter<>(); - security.doProofOfWork(objectMessage, 1000, 1000, + crypto.doProofOfWork(objectMessage, 1000, 1000, new ProofOfWorkEngine.Callback() { @Override public void onNonceCalculated(byte[] initialHash, byte[] nonce) { @@ -96,6 +102,56 @@ public class CryptographyTest { } }); objectMessage.setNonce(waiter.waitForValue()); - security.checkProofOfWork(objectMessage, 1000, 1000); + try { + crypto.checkProofOfWork(objectMessage, 1000, 1000); + } catch (InsufficientProofOfWorkException e) { + fail(e.getMessage()); + } } -} \ No newline at end of file + + @Test + public void ensureEncryptionAndDecryptionWorks() { + byte[] data = crypto.randomBytes(100); + byte[] key_e = crypto.randomBytes(32); + byte[] iv = crypto.randomBytes(16); + byte[] encrypted = crypto.crypt(true, data, key_e, iv); + byte[] decrypted = crypto.crypt(false, encrypted, key_e, iv); + assertArrayEquals(data, decrypted); + } + + @Test(expected = IllegalArgumentException.class) + public void ensureDecryptionFailsWithInvalidCypherText() { + byte[] data = crypto.randomBytes(128); + byte[] key_e = crypto.randomBytes(32); + byte[] iv = crypto.randomBytes(16); + crypto.crypt(false, data, key_e, iv); + } + + @Test + public void testMultiplication() { + byte[] a = crypto.randomBytes(PrivateKey.PRIVATE_KEY_SIZE); + byte[] A = crypto.createPublicKey(a); + + byte[] b = crypto.randomBytes(PrivateKey.PRIVATE_KEY_SIZE); + byte[] B = crypto.createPublicKey(b); + + assertArrayEquals(crypto.multiply(A, b), crypto.multiply(B, a)); + } + + @Test + public void ensureSignatureIsValid() { + byte[] data = crypto.randomBytes(100); + PrivateKey privateKey = new PrivateKey(false, 1, 1000, 1000); + byte[] signature = crypto.getSignature(data, privateKey); + assertThat(crypto.isSignatureValid(data, signature, privateKey.getPubkey()), is(true)); + } + + @Test + public void ensureSignatureIsInvalidForTemperedData() { + byte[] data = crypto.randomBytes(100); + PrivateKey privateKey = new PrivateKey(false, 1, 1000, 1000); + byte[] signature = crypto.getSignature(data, privateKey); + data[0]++; + assertThat(crypto.isSignatureValid(data, signature, privateKey.getPubkey()), is(false)); + } +} diff --git a/cryptography-sc/build.gradle b/cryptography-sc/build.gradle index 16771fc..a052c2e 100644 --- a/cryptography-sc/build.gradle +++ b/cryptography-sc/build.gradle @@ -13,5 +13,5 @@ uploadArchives { dependencies { compile project(':core') compile 'com.madgag.spongycastle:prov:1.52.0.0' - testCompile 'junit:junit:4.11' + testCompile 'junit:junit:4.12' } 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 c9506fb..b90e1c8 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 @@ -18,6 +18,7 @@ package ch.dissem.bitmessage.cryptography.sc; import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.ports.AbstractCryptography; import org.spongycastle.asn1.x9.X9ECParameters; import org.spongycastle.crypto.BufferedBlockCipher; @@ -37,6 +38,7 @@ import org.spongycastle.jce.spec.ECPublicKeySpec; import org.spongycastle.math.ec.ECPoint; import java.math.BigInteger; +import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.PublicKey; import java.security.Signature; @@ -49,19 +51,23 @@ 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"; + private static final String PROVIDER = "SC"; static { java.security.Security.addProvider(new BouncyCastleProvider()); } public SpongyCryptography() { - super("SC"); + super(PROVIDER); } @Override public byte[] crypt(boolean encrypt, byte[] data, byte[] key_e, byte[] initializationVector) { - BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()), new PKCS7Padding()); - + BufferedBlockCipher cipher = new PaddedBufferedBlockCipher( + new CBCBlockCipher(new AESEngine()), + new PKCS7Padding() + ); CipherParameters params = new ParametersWithIV(new KeyParameter(key_e), initializationVector); cipher.init(encrypt, params); @@ -103,14 +109,14 @@ public class SpongyCryptography extends AbstractCryptography { ECPoint Q = keyToPoint(pubkey.getSigningKey()); KeySpec keySpec = new ECPublicKeySpec(Q, spec); - PublicKey publicKey = KeyFactory.getInstance("ECDSA", "SC").generatePublic(keySpec); + PublicKey publicKey = KeyFactory.getInstance(ALGORITHM_ECDSA, PROVIDER).generatePublic(keySpec); - Signature sig = Signature.getInstance("ECDSA", "SC"); + Signature sig = Signature.getInstance(ALGORITHM_ECDSA, PROVIDER); sig.initVerify(publicKey); sig.update(data); return sig.verify(signature); - } catch (Exception e) { - throw new RuntimeException(e); + } catch (GeneralSecurityException e) { + throw new ApplicationException(e); } } @@ -127,14 +133,15 @@ public class SpongyCryptography extends AbstractCryptography { BigInteger d = keyToBigInt(privateKey.getPrivateSigningKey()); KeySpec keySpec = new ECPrivateKeySpec(d, spec); - java.security.PrivateKey privKey = KeyFactory.getInstance("ECDSA", "SC").generatePrivate(keySpec); + java.security.PrivateKey privKey = KeyFactory.getInstance(ALGORITHM_ECDSA, PROVIDER) + .generatePrivate(keySpec); - Signature sig = Signature.getInstance("ECDSA", "SC"); + Signature sig = Signature.getInstance(ALGORITHM_ECDSA, PROVIDER); sig.initSign(privKey); sig.update(data); return sig.sign(); - } catch (Exception e) { - throw new RuntimeException(e); + } catch (GeneralSecurityException e) { + throw new ApplicationException(e); } } diff --git a/demo/build.gradle b/demo/build.gradle index d87b0f3..79f5834 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -30,7 +30,8 @@ dependencies { compile project(':wif') compile 'org.slf4j:slf4j-simple:1.7.12' compile 'args4j:args4j:2.32' - compile 'com.h2database:h2:1.4.190' - testCompile 'junit:junit:4.11' + compile 'com.h2database:h2:1.4.192' + compile 'org.apache.commons:commons-lang3:3.4' + testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' } 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 26d6652..3861831 100644 --- a/demo/src/main/java/ch/dissem/bitmessage/demo/Application.java +++ b/demo/src/main/java/ch/dissem/bitmessage/demo/Application.java @@ -20,55 +20,37 @@ import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.payload.Pubkey; -import ch.dissem.bitmessage.networking.DefaultNetworkHandler; -import ch.dissem.bitmessage.ports.MemoryNodeRegistry; -import ch.dissem.bitmessage.repository.*; -import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; +import ch.dissem.bitmessage.entity.valueobject.Label; +import org.apache.commons.lang3.text.WordUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.util.List; -import java.util.Scanner; +import java.util.stream.Collectors; + +import static ch.dissem.bitmessage.demo.CommandLine.COMMAND_BACK; +import static ch.dissem.bitmessage.demo.CommandLine.ERROR_UNKNOWN_COMMAND; /** * A simple command line Bitmessage application */ public class Application { private final static Logger LOG = LoggerFactory.getLogger(Application.class); - private final Scanner scanner; + private final CommandLine commandLine; private BitmessageContext ctx; - public Application(String syncServer, int syncPort) { - JdbcConfig jdbcConfig = new JdbcConfig(); - ctx = new BitmessageContext.Builder() - .addressRepo(new JdbcAddressRepository(jdbcConfig)) - .inventory(new JdbcInventory(jdbcConfig)) - .nodeRegistry(new MemoryNodeRegistry()) - .messageRepo(new JdbcMessageRepository(jdbcConfig)) - .powRepo(new JdbcProofOfWorkRepository(jdbcConfig)) - .networkHandler(new DefaultNetworkHandler()) - .cryptography(new BouncyCryptography()) - .port(48444) - .listener(new BitmessageContext.Listener() { - @Override - public void receive(Plaintext plaintext) { - try { - System.out.println(new String(plaintext.getMessage(), "UTF-8")); - } catch (UnsupportedEncodingException e) { - LOG.error(e.getMessage(), e); - } - } - }) - .build(); + public Application(BitmessageContext.Builder ctxBuilder, InetAddress syncServer, int syncPort) { + ctx = ctxBuilder + .listener(plaintext -> System.out.println("New Message from " + plaintext.getFrom() + ": " + plaintext.getSubject())) + .build(); if (syncServer == null) { ctx.startup(); } - scanner = new Scanner(System.in); + commandLine = new CommandLine(); String command; do { @@ -84,7 +66,7 @@ public class Application { System.out.println("?) info"); System.out.println("e) exit"); - command = nextCommand(); + command = commandLine.nextCommand(); try { switch (command) { case "i": { @@ -98,7 +80,7 @@ public class Application { subscriptions(); break; case "m": - messages(); + labels(); break; case "?": info(); @@ -106,10 +88,12 @@ public class Application { case "e": break; case "y": - ctx.synchronize(InetAddress.getByName(syncServer), syncPort, 120, true); + if (syncServer != null) { + ctx.synchronize(syncServer, syncPort, 120, true); + } break; default: - System.out.println("Unknown command. Please try again."); + System.out.println(ERROR_UNKNOWN_COMMAND); } } catch (Exception e) { LOG.debug(e.getMessage()); @@ -121,12 +105,27 @@ public class Application { } private void info() { - System.out.println(); - System.out.println(ctx.status()); - } + String command; + do { + System.out.println(); + System.out.println(ctx.status()); + System.out.println(); + System.out.println("c) cleanup inventory"); + System.out.println("r) resend unacknowledged messages"); + System.out.println(COMMAND_BACK); - private String nextCommand() { - return scanner.nextLine().trim().toLowerCase(); + command = commandLine.nextCommand(); + switch (command) { + case "c": + ctx.cleanup(); + break; + case "r": + ctx.resendUnacknowledgedMessages(); + break; + case "b": + return; + } + } while (!"b".equals(command)); } private void identities() { @@ -134,28 +133,21 @@ public class Application { List<BitmessageAddress> identities = ctx.addresses().getIdentities(); do { System.out.println(); - int i = 0; - for (BitmessageAddress identity : identities) { - i++; - System.out.print(i + ") "); - if (identity.getAlias() != null) { - System.out.println(identity.getAlias() + " (" + identity.getAddress() + ")"); - } else { - System.out.println(identity.getAddress()); - } - } - if (i == 0) { - System.out.println("You have no identities yet."); - } + commandLine.listAddresses(identities, "identities"); System.out.println("a) create identity"); - System.out.println("b) back"); + System.out.println("c) join chan"); + System.out.println(COMMAND_BACK); - command = nextCommand(); + command = commandLine.nextCommand(); switch (command) { case "a": addIdentity(); identities = ctx.addresses().getIdentities(); break; + case "c": + joinChan(); + identities = ctx.addresses().getIdentities(); + break; case "b": return; default: @@ -163,7 +155,7 @@ public class Application { int index = Integer.parseInt(command) - 1; address(identities.get(index)); } catch (NumberFormatException e) { - System.out.println("Unknown command. Please try again."); + System.out.println(ERROR_UNKNOWN_COMMAND); } } } while (!"b".equals(command)); @@ -171,38 +163,35 @@ public class Application { private void addIdentity() { System.out.println(); - BitmessageAddress identity = ctx.createIdentity(yesNo("would you like a shorter address? This will take some time to calculate."), Pubkey.Feature.DOES_ACK); + BitmessageAddress identity = ctx.createIdentity(commandLine.yesNo("would you like a shorter address? This will take some time to calculate."), Pubkey.Feature.DOES_ACK); System.out.println("Please enter an alias for this identity, or an empty string for none"); - String alias = scanner.nextLine().trim(); + String alias = commandLine.nextLineTrimmed(); if (alias.length() > 0) { identity.setAlias(alias); } ctx.addresses().save(identity); } + private void joinChan() { + System.out.println(); + System.out.print("Passphrase: "); + String passphrase = commandLine.nextLine(); + System.out.print("Address: "); + String address = commandLine.nextLineTrimmed(); + ctx.joinChan(passphrase, address); + } + private void contacts() { String command; List<BitmessageAddress> contacts = ctx.addresses().getContacts(); do { System.out.println(); - int i = 0; - for (BitmessageAddress contact : contacts) { - i++; - System.out.print(i + ") "); - if (contact.getAlias() != null) { - System.out.println(contact.getAlias() + " (" + contact.getAddress() + ")"); - } else { - System.out.println(contact.getAddress()); - } - } - if (i == 0) { - System.out.println("You have no contacts yet."); - } + commandLine.listAddresses(contacts, "contacts"); System.out.println(); System.out.println("a) add contact"); - System.out.println("b) back"); + System.out.println(COMMAND_BACK); - command = nextCommand(); + command = commandLine.nextCommand(); switch (command) { case "a": addContact(false); @@ -215,7 +204,7 @@ public class Application { int index = Integer.parseInt(command) - 1; address(contacts.get(index)); } catch (NumberFormatException e) { - System.out.println("Unknown command. Please try again."); + System.out.println(ERROR_UNKNOWN_COMMAND); } } } while (!"b".equals(command)); @@ -225,9 +214,9 @@ public class Application { System.out.println(); System.out.println("Please enter the Bitmessage address you want to add"); try { - BitmessageAddress address = new BitmessageAddress(scanner.nextLine().trim()); + BitmessageAddress address = new BitmessageAddress(commandLine.nextLineTrimmed()); System.out.println("Please enter an alias for this address, or an empty string for none"); - String alias = scanner.nextLine().trim(); + String alias = commandLine.nextLineTrimmed(); if (alias.length() > 0) { address.setAlias(alias); } @@ -245,24 +234,12 @@ public class Application { List<BitmessageAddress> subscriptions = ctx.addresses().getSubscriptions(); do { System.out.println(); - int i = 0; - for (BitmessageAddress contact : subscriptions) { - i++; - System.out.print(i + ") "); - if (contact.getAlias() != null) { - System.out.println(contact.getAlias() + " (" + contact.getAddress() + ")"); - } else { - System.out.println(contact.getAddress()); - } - } - if (i == 0) { - System.out.println("You have no subscriptions yet."); - } + commandLine.listAddresses(subscriptions, "subscriptions"); System.out.println(); System.out.println("a) add subscription"); - System.out.println("b) back"); + System.out.println(COMMAND_BACK); - command = nextCommand(); + command = commandLine.nextCommand(); switch (command) { case "a": addContact(true); @@ -275,7 +252,7 @@ public class Application { int index = Integer.parseInt(command) - 1; address(subscriptions.get(index)); } catch (NumberFormatException e) { - System.out.println("Unknown command. Please try again."); + System.out.println(ERROR_UNKNOWN_COMMAND); } } } while (!"b".equals(command)); @@ -289,34 +266,47 @@ public class Application { System.out.println("Stream: " + address.getStream()); System.out.println("Version: " + address.getVersion()); if (address.getPrivateKey() == null) { - if (address.getPubkey() != null) { - System.out.println("Public key available"); - } else { + if (address.getPubkey() == null) { System.out.println("Public key still missing"); + } else { + System.out.println("Public key available"); + } + } else { + if (address.isChan()) { + System.out.println("Chan"); + } else { + System.out.println("Identity"); } } } - private void messages() { + private void labels() { + List<Label> labels = ctx.messages().getLabels(); String command; - List<Plaintext> messages = ctx.messages().findMessages(Plaintext.Status.RECEIVED); do { System.out.println(); int i = 0; - for (Plaintext message : messages) { + for (Label label : labels) { i++; - System.out.println(i + ") From: " + message.getFrom() + "; Subject: " + message.getSubject()); - } - if (i == 0) { - System.out.println("You have no messages."); + System.out.print(i + ") " + label); + int unread = ctx.messages().countUnread(label); + if (unread > 0) { + System.out.println(" [" + unread + "]"); + } else { + System.out.println(); + } } + System.out.println("a) Archive"); System.out.println(); System.out.println("c) compose message"); System.out.println("s) compose broadcast"); - System.out.println("b) back"); + System.out.println(COMMAND_BACK); - command = scanner.nextLine().trim(); + command = commandLine.nextCommand(); switch (command) { + case "a": + messages(null); + break; case "c": compose(false); break; @@ -325,12 +315,56 @@ public class Application { break; case "b": return; + default: + try { + int index = Integer.parseInt(command) - 1; + messages(labels.get(index)); + } catch (NumberFormatException | IndexOutOfBoundsException e) { + System.out.println(ERROR_UNKNOWN_COMMAND); + } + } + } while (!"b".equalsIgnoreCase(command)); + } + + private void messages(Label label) { + String command; + do { + List<Plaintext> messages = ctx.messages().findMessages(label); + System.out.println(); + int i = 0; + for (Plaintext message : messages) { + i++; + System.out.println(i + (message.isUnread() ? ">" : ")") + " From: " + message.getFrom() + "; Subject: " + message.getSubject()); + } + if (i == 0) { + System.out.println("There are no messages."); + } + System.out.println(); + System.out.println("c) compose message"); + System.out.println("s) compose broadcast"); + if (label.getType() == Label.Type.TRASH) { + System.out.println("e) empty trash"); + } + System.out.println(COMMAND_BACK); + + command = commandLine.nextCommand(); + switch (command) { + case "c": + compose(false); + break; + case "s": + compose(true); + break; + case "e": + messages.forEach(ctx.messages()::remove); + case "b": + return; default: try { int index = Integer.parseInt(command) - 1; show(messages.get(index)); } catch (NumberFormatException | IndexOutOfBoundsException e) { - System.out.println("Unknown command. Please try again."); + System.out.println(ERROR_UNKNOWN_COMMAND); } } } while (!"b".equalsIgnoreCase(command)); @@ -342,37 +376,47 @@ public class Application { System.out.println("To: " + message.getTo()); System.out.println("Subject: " + message.getSubject()); System.out.println(); - System.out.println(message.getText()); + System.out.println(WordUtils.wrap(message.getText(), 120)); System.out.println(); - System.out.println("Labels: " + message.getLabels()); + System.out.println(message.getLabels().stream().map(Label::toString).collect( + Collectors.joining(", ", "Labels: ", ""))); System.out.println(); + ctx.labeler().markAsRead(message); + ctx.messages().save(message); String command; do { System.out.println("r) reply"); System.out.println("d) delete"); - System.out.println("b) back"); - command = nextCommand(); + System.out.println("a) archive"); + System.out.println(COMMAND_BACK); + command = commandLine.nextCommand(); switch (command) { case "r": compose(message.getTo(), message.getFrom(), "RE: " + message.getSubject()); break; case "d": - ctx.messages().remove(message); + ctx.labeler().delete(message); + ctx.messages().save(message); + return; + case "a": + ctx.labeler().archive(message); + ctx.messages().save(message); + return; case "b": return; default: - System.out.println("Unknown command. Please try again."); + System.out.println(ERROR_UNKNOWN_COMMAND); } } while (!"b".equalsIgnoreCase(command)); } private void compose(boolean broadcast) { System.out.println(); - BitmessageAddress from = selectAddress(true); + BitmessageAddress from = selectIdentity(); if (from == null) { return; } - BitmessageAddress to = (broadcast ? null : selectAddress(false)); + BitmessageAddress to = (broadcast ? null : selectContact()); if (!broadcast && to == null) { return; } @@ -380,58 +424,22 @@ public class Application { compose(from, to, null); } - private BitmessageAddress selectAddress(boolean id) { - List<BitmessageAddress> addresses = (id ? ctx.addresses().getIdentities() : ctx.addresses().getContacts()); + private BitmessageAddress selectIdentity() { + List<BitmessageAddress> addresses = ctx.addresses().getIdentities(); while (addresses.size() == 0) { - if (id) { - addIdentity(); - addresses = ctx.addresses().getIdentities(); - } else { - addContact(false); - addresses = ctx.addresses().getContacts(); - } + addIdentity(); + addresses = ctx.addresses().getIdentities(); } - if (addresses.size() == 1) { - return addresses.get(0); + return commandLine.selectAddress(addresses, "From:"); + } + + private BitmessageAddress selectContact() { + List<BitmessageAddress> addresses = ctx.addresses().getContacts(); + while (addresses.size() == 0) { + addContact(false); + addresses = ctx.addresses().getContacts(); } - - String command; - do { - System.out.println(); - if (id) { - System.out.println("From:"); - } else { - System.out.println("To:"); - } - - int i = 0; - for (BitmessageAddress identity : addresses) { - i++; - System.out.print(i + ") "); - if (identity.getAlias() != null) { - System.out.println(identity.getAlias() + " (" + identity.getAddress() + ")"); - } else { - System.out.println(identity.getAddress()); - } - } - System.out.println("b) back"); - - command = nextCommand(); - switch (command) { - case "b": - return null; - default: - try { - int index = Integer.parseInt(command) - 1; - if (addresses.get(index) != null) { - return addresses.get(index); - } - } catch (NumberFormatException e) { - System.out.println("Unknown command. Please try again."); - } - } - } while (!"b".equals(command)); - return null; + return commandLine.selectAddress(addresses, "To:"); } private void compose(BitmessageAddress from, BitmessageAddress to, String subject) { @@ -445,29 +453,19 @@ public class Application { System.out.println("Subject: " + subject); } else { System.out.print("Subject: "); - subject = scanner.nextLine().trim(); + subject = commandLine.nextLineTrimmed(); } System.out.println("Message:"); StringBuilder message = new StringBuilder(); String line; do { - line = scanner.nextLine(); + line = commandLine.nextLine(); message.append(line).append('\n'); - } while (line.length() > 0 || !yesNo("Send message?")); + } while (line.length() > 0 || !commandLine.yesNo("Send message?")); if (broadcast) { ctx.broadcast(from, subject, message.toString()); } else { ctx.send(from, to, subject, message.toString()); } } - - private boolean yesNo(String question) { - String answer; - do { - System.out.println(question + " (y/n)"); - answer = scanner.nextLine(); - if ("y".equalsIgnoreCase(answer)) return true; - if ("n".equalsIgnoreCase(answer)) return false; - } while (true); - } } diff --git a/demo/src/main/java/ch/dissem/bitmessage/demo/CommandLine.java b/demo/src/main/java/ch/dissem/bitmessage/demo/CommandLine.java new file mode 100644 index 0000000..f04196a --- /dev/null +++ b/demo/src/main/java/ch/dissem/bitmessage/demo/CommandLine.java @@ -0,0 +1,102 @@ +/* + * Copyright 2016 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.demo; + +import ch.dissem.bitmessage.entity.BitmessageAddress; + +import java.util.List; +import java.util.Scanner; + + +/** + * @author Christian Basler + */ +public class CommandLine { + public static final String COMMAND_BACK = "b) back"; + public static final String ERROR_UNKNOWN_COMMAND = "Unknown command. Please try again."; + + private Scanner scanner = new Scanner(System.in); + + public String nextCommand() { + return scanner.nextLine().trim().toLowerCase(); + } + + public String nextLine() { + return scanner.nextLine(); + } + + public String nextLineTrimmed() { + return scanner.nextLine(); + } + + public boolean yesNo(String question) { + String answer; + do { + System.out.println(question + " (y/n)"); + answer = scanner.nextLine(); + if ("y".equalsIgnoreCase(answer)) return true; + if ("n".equalsIgnoreCase(answer)) return false; + } while (true); + } + + public BitmessageAddress selectAddress(List<BitmessageAddress> addresses, String label) { + if (addresses.size() == 1) { + return addresses.get(0); + } + + String command; + do { + System.out.println(); + System.out.println(label); + + listAddresses(addresses, "contacts"); + System.out.println(COMMAND_BACK); + + command = nextCommand(); + switch (command) { + case "b": + return null; + default: + try { + int index = Integer.parseInt(command) - 1; + if (addresses.get(index) != null) { + return addresses.get(index); + } + } catch (NumberFormatException e) { + System.out.println(ERROR_UNKNOWN_COMMAND); + } + } + } while (!"b".equals(command)); + return null; + } + + public void listAddresses(List<BitmessageAddress> addresses, String kind) { + int i = 0; + for (BitmessageAddress address : addresses) { + i++; + System.out.print(i + ") "); + if (address.getAlias() == null) { + System.out.println(address.getAddress()); + } else { + System.out.println(address.getAlias() + " (" + address.getAddress() + ")"); + } + } + if (i == 0) { + System.out.println("You have no " + kind + " yet."); + } + } +} 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 b0e114b..c2cc7a5 100644 --- a/demo/src/main/java/ch/dissem/bitmessage/demo/Main.java +++ b/demo/src/main/java/ch/dissem/bitmessage/demo/Main.java @@ -17,20 +17,29 @@ package ch.dissem.bitmessage.demo; import ch.dissem.bitmessage.BitmessageContext; -import ch.dissem.bitmessage.networking.DefaultNetworkHandler; -import ch.dissem.bitmessage.ports.MemoryNodeRegistry; -import ch.dissem.bitmessage.repository.*; import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.networking.nio.NioNetworkHandler; +import ch.dissem.bitmessage.ports.NodeRegistry; +import ch.dissem.bitmessage.repository.*; import ch.dissem.bitmessage.wif.WifExporter; import ch.dissem.bitmessage.wif.WifImporter; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.Option; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; +import java.net.InetAddress; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; public class Main { + private static final Logger LOG = LoggerFactory.getLogger(Main.class); + public static void main(String[] args) throws IOException { if (System.getProperty("org.slf4j.simpleLogger.defaultLogLevel") == null) System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "ERROR"); @@ -44,18 +53,39 @@ public class Main { } catch (CmdLineException e) { parser.printUsage(System.err); } + + JdbcConfig jdbcConfig = new JdbcConfig(); + BitmessageContext.Builder ctxBuilder = new BitmessageContext.Builder() + .addressRepo(new JdbcAddressRepository(jdbcConfig)) + .inventory(new JdbcInventory(jdbcConfig)) + .messageRepo(new JdbcMessageRepository(jdbcConfig)) + .powRepo(new JdbcProofOfWorkRepository(jdbcConfig)) + .networkHandler(new NioNetworkHandler()) + .cryptography(new BouncyCryptography()) + .port(48444); + if (options.localPort != null) { + ctxBuilder.nodeRegistry(new NodeRegistry() { + @Override + public List<NetworkAddress> getKnownAddresses(int limit, long... streams) { + return Arrays.stream(streams) + .mapToObj(s -> new NetworkAddress.Builder() + .ipv4(127, 0, 0, 1) + .port(options.localPort) + .stream(s).build()) + .collect(Collectors.toList()); + } + + @Override + public void offerAddresses(List<NetworkAddress> addresses) { + LOG.info("Local node registry ignored offered addresses: " + addresses); + } + }); + } else { + ctxBuilder.nodeRegistry(new JdbcNodeRegistry(jdbcConfig)); + } + if (options.exportWIF != null || options.importWIF != null) { - JdbcConfig jdbcConfig = new JdbcConfig(); - BitmessageContext ctx = new BitmessageContext.Builder() - .addressRepo(new JdbcAddressRepository(jdbcConfig)) - .inventory(new JdbcInventory(jdbcConfig)) - .nodeRegistry(new MemoryNodeRegistry()) - .messageRepo(new JdbcMessageRepository(jdbcConfig)) - .powRepo(new JdbcProofOfWorkRepository(jdbcConfig)) - .networkHandler(new DefaultNetworkHandler()) - .cryptography(new BouncyCryptography()) - .port(48444) - .build(); + BitmessageContext ctx = ctxBuilder.build(); if (options.exportWIF != null) { new WifExporter(ctx).addAll().write(options.exportWIF); @@ -64,11 +94,15 @@ public class Main { new WifImporter(ctx, options.importWIF).importAll(); } } else { - new Application(options.syncServer, options.syncPort); + InetAddress syncServer = options.syncServer == null ? null : InetAddress.getByName(options.syncServer); + new Application(ctxBuilder, syncServer, options.syncPort); } } private static class CmdLineOptions { + @Option(name = "-local", usage = "Connect to local Bitmessage client on given port, instead of the usual connections from node.txt") + private Integer localPort; + @Option(name = "-import", usage = "Import from keys.dat or other WIF file.") private File importWIF; diff --git a/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java b/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java index 1309336..dac6f74 100644 --- a/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java +++ b/demo/src/test/java/ch/dissem/bitmessage/SystemTest.java @@ -4,33 +4,67 @@ import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.networking.DefaultNetworkHandler; +import ch.dissem.bitmessage.networking.nio.NioNetworkHandler; +import ch.dissem.bitmessage.ports.DefaultLabeler; +import ch.dissem.bitmessage.ports.Labeler; +import ch.dissem.bitmessage.ports.NetworkHandler; import ch.dissem.bitmessage.repository.*; import ch.dissem.bitmessage.utils.TTL; -import org.junit.AfterClass; -import org.junit.BeforeClass; +import org.junit.After; +import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.Arrays; +import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; +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; /** * @author Christian Basler */ +@RunWith(Parameterized.class) public class SystemTest { - static BitmessageContext alice; - static TestListener aliceListener = new TestListener(); - static BitmessageAddress aliceIdentity; + private static int port = 6000; + private final NetworkHandler aliceNetworkHandler; + private final NetworkHandler bobNetworkHandler; - static BitmessageContext bob; - static TestListener bobListener = new TestListener(); - static BitmessageAddress bobIdentity; + private BitmessageContext alice; + private TestListener aliceListener = new TestListener(); + private Labeler aliceLabeler = Mockito.spy(new DebugLabeler("Alice")); + private BitmessageAddress aliceIdentity; - @BeforeClass - public static void setUp() { + private BitmessageContext bob; + private TestListener bobListener = new TestListener(); + private BitmessageAddress bobIdentity; + + public SystemTest(NetworkHandler peer, NetworkHandler node) { + this.aliceNetworkHandler = peer; + this.bobNetworkHandler = node; + } + + @Parameterized.Parameters + public static List<Object[]> parameters() { + return Arrays.asList(new Object[][]{ + {new NioNetworkHandler(), new DefaultNetworkHandler()}, + {new NioNetworkHandler(), new NioNetworkHandler()} + }); + } + + @Before + public void setUp() { + int alicePort = port++; + int bobPort = port++; TTL.msg(5 * MINUTE); TTL.getpubkey(5 * MINUTE); TTL.pubkey(5 * MINUTE); @@ -40,14 +74,15 @@ public class SystemTest { .inventory(new JdbcInventory(aliceDB)) .messageRepo(new JdbcMessageRepository(aliceDB)) .powRepo(new JdbcProofOfWorkRepository(aliceDB)) - .port(6001) - .nodeRegistry(new TestNodeRegistry(6002)) - .networkHandler(new DefaultNetworkHandler()) + .port(alicePort) + .nodeRegistry(new TestNodeRegistry(bobPort)) + .networkHandler(aliceNetworkHandler) .cryptography(new BouncyCryptography()) .listener(aliceListener) + .labeler(aliceLabeler) .build(); alice.startup(); - aliceIdentity = alice.createIdentity(false); + aliceIdentity = alice.createIdentity(false, DOES_ACK); JdbcConfig bobDB = new JdbcConfig("jdbc:h2:mem:bob;DB_CLOSE_DELAY=-1", "sa", ""); bob = new BitmessageContext.Builder() @@ -55,30 +90,128 @@ public class SystemTest { .inventory(new JdbcInventory(bobDB)) .messageRepo(new JdbcMessageRepository(bobDB)) .powRepo(new JdbcProofOfWorkRepository(bobDB)) - .port(6002) - .nodeRegistry(new TestNodeRegistry(6001)) - .networkHandler(new DefaultNetworkHandler()) + .port(bobPort) + .nodeRegistry(new TestNodeRegistry(alicePort)) + .networkHandler(bobNetworkHandler) .cryptography(new BouncyCryptography()) .listener(bobListener) + .labeler(new DebugLabeler("Bob")) .build(); bob.startup(); - bobIdentity = bob.createIdentity(false); + bobIdentity = bob.createIdentity(false, DOES_ACK); + + ((DebugLabeler) alice.labeler()).init(aliceIdentity, bobIdentity); + ((DebugLabeler) bob.labeler()).init(aliceIdentity, bobIdentity); } - @AfterClass - public static void tearDown() { + @After + public void tearDown() { alice.shutdown(); bob.shutdown(); } - @Test + @Test(timeout = 60_000) public void ensureAliceCanSendMessageToBob() throws Exception { - bobListener.reset(); String originalMessage = UUID.randomUUID().toString(); alice.send(aliceIdentity, new BitmessageAddress(bobIdentity.getAddress()), "Subject", originalMessage); Plaintext plaintext = bobListener.get(15, TimeUnit.MINUTES); + assertThat(plaintext.getType(), equalTo(Plaintext.Type.MSG)); + assertThat(plaintext.getText(), equalTo(originalMessage)); + + Mockito.verify(aliceLabeler, Mockito.timeout(TimeUnit.MINUTES.toMillis(15)).atLeastOnce()) + .markAsAcknowledged(any()); + } + + @Test(timeout = 30_000) + public void ensureBobCanReceiveBroadcastFromAlice() throws Exception { + String originalMessage = UUID.randomUUID().toString(); + bob.addSubscribtion(new BitmessageAddress(aliceIdentity.getAddress())); + alice.broadcast(aliceIdentity, "Subject", originalMessage); + + Plaintext plaintext = bobListener.get(15, TimeUnit.MINUTES); + + assertThat(plaintext.getType(), equalTo(Plaintext.Type.BROADCAST)); assertThat(plaintext.getText(), equalTo(originalMessage)); } + + private static class DebugLabeler extends DefaultLabeler { + private final Logger LOG = LoggerFactory.getLogger("Labeler"); + final String name; + String alice; + String bob; + + private DebugLabeler(String name) { + this.name = name; + } + + private void init(BitmessageAddress alice, BitmessageAddress bob) { + this.alice = alice.getAddress(); + this.bob = bob.getAddress(); + } + + @Override + public void setLabels(Plaintext msg) { + LOG.info(name + ": From " + name(msg.getFrom()) + ": Received"); + super.setLabels(msg); + } + + @Override + public void markAsDraft(Plaintext msg) { + LOG.info(name + ": From " + name(msg.getFrom()) + ": Draft"); + super.markAsDraft(msg); + } + + @Override + public void markAsSending(Plaintext msg) { + LOG.info(name + ": From " + name(msg.getFrom()) + ": Sending"); + super.markAsSending(msg); + } + + @Override + public void markAsSent(Plaintext msg) { + LOG.info(name + ": From " + name(msg.getFrom()) + ": Sent"); + super.markAsSent(msg); + } + + @Override + public void markAsAcknowledged(Plaintext msg) { + LOG.info(name + ": From " + name(msg.getFrom()) + ": Acknowledged"); + super.markAsAcknowledged(msg); + } + + @Override + public void markAsRead(Plaintext msg) { + LOG.info(name + ": From " + name(msg.getFrom()) + ": Read"); + super.markAsRead(msg); + } + + @Override + public void markAsUnread(Plaintext msg) { + LOG.info(name + ": From " + name(msg.getFrom()) + ": Unread"); + super.markAsUnread(msg); + } + + @Override + public void delete(Plaintext msg) { + LOG.info(name + ": From " + name(msg.getFrom()) + ": Cleared"); + super.delete(msg); + } + + @Override + public void archive(Plaintext msg) { + LOG.info(name + ": From " + name(msg.getFrom()) + ": Archived"); + super.archive(msg); + } + + private String name(BitmessageAddress address) { + if (alice.equals(address.getAddress())) + return "Alice"; + else if (bob.equals(address.getAddress())) + return "Bob"; + else + return "Unknown (" + address.getAddress() + ")"; + } + } } diff --git a/extensions/build.gradle b/extensions/build.gradle index d44f900..42b175b 100644 --- a/extensions/build.gradle +++ b/extensions/build.gradle @@ -28,7 +28,7 @@ uploadArchives { dependencies { compile project(':core') - testCompile 'junit:junit:4.11' + testCompile 'junit:junit:4.12' testCompile 'org.slf4j:slf4j-simple:1.7.12' testCompile 'org.mockito:mockito-core:1.10.19' testCompile project(path: ':core', configuration: 'testArtifacts') diff --git a/extensions/src/main/java/ch/dissem/bitmessage/extensions/CryptoCustomMessage.java b/extensions/src/main/java/ch/dissem/bitmessage/extensions/CryptoCustomMessage.java index 49c6f1b..7955e5b 100644 --- a/extensions/src/main/java/ch/dissem/bitmessage/extensions/CryptoCustomMessage.java +++ b/extensions/src/main/java/ch/dissem/bitmessage/extensions/CryptoCustomMessage.java @@ -28,7 +28,7 @@ import ch.dissem.bitmessage.utils.Encode; import java.io.*; import static ch.dissem.bitmessage.utils.Decode.*; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; /** * A {@link CustomMessage} implementation that contains signed and encrypted data. @@ -36,7 +36,10 @@ import static ch.dissem.bitmessage.utils.Singleton.security; * @author Christian Basler */ public class CryptoCustomMessage<T extends Streamable> extends CustomMessage { + private static final long serialVersionUID = 7395193565986284426L; + public static final String COMMAND = "ENCRYPTED"; + private final Reader<T> dataReader; private CryptoBox container; private BitmessageAddress sender; @@ -77,7 +80,7 @@ public class CryptoCustomMessage<T extends Streamable> extends CustomMessage { } data.write(out); - Encode.varBytes(security().getSignature(out.toByteArray(), identity.getPrivateKey()), out); + Encode.varBytes(cryptography().getSignature(out.toByteArray(), identity.getPrivateKey()), out); container = new CryptoBox(out.toByteArray(), publicKey); } @@ -134,9 +137,9 @@ public class CryptoCustomMessage<T extends Streamable> extends CustomMessage { return read; } - public void checkSignature(Pubkey pubkey) throws IOException, RuntimeException { - if (!security().isSignatureValid(out.toByteArray(), varBytes(wrapped), pubkey)) { - throw new RuntimeException("Signature check failed"); + public void checkSignature(Pubkey pubkey) throws IOException, IllegalStateException { + if (!cryptography().isSignatureValid(out.toByteArray(), varBytes(wrapped), pubkey)) { + throw new IllegalStateException("Signature check failed"); } } } diff --git a/extensions/src/main/java/ch/dissem/bitmessage/extensions/pow/ProofOfWorkRequest.java b/extensions/src/main/java/ch/dissem/bitmessage/extensions/pow/ProofOfWorkRequest.java index 0024aaa..e5ba4f8 100644 --- a/extensions/src/main/java/ch/dissem/bitmessage/extensions/pow/ProofOfWorkRequest.java +++ b/extensions/src/main/java/ch/dissem/bitmessage/extensions/pow/ProofOfWorkRequest.java @@ -24,6 +24,7 @@ import ch.dissem.bitmessage.utils.Encode; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.ByteBuffer; import java.util.Arrays; import static ch.dissem.bitmessage.utils.Decode.*; @@ -32,6 +33,8 @@ import static ch.dissem.bitmessage.utils.Decode.*; * @author Christian Basler */ public class ProofOfWorkRequest implements Streamable { + private static final long serialVersionUID = 4729003751499662713L; + private final BitmessageAddress sender; private final byte[] initialHash; private final Request request; @@ -81,6 +84,13 @@ public class ProofOfWorkRequest implements Streamable { Encode.varBytes(data, out); } + @Override + public void write(ByteBuffer buffer) { + buffer.put(initialHash); + Encode.varString(request.name(), buffer); + Encode.varBytes(data, buffer); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/extensions/src/test/java/ch/dissem/bitmessage/extensions/CryptoCustomMessageTest.java b/extensions/src/test/java/ch/dissem/bitmessage/extensions/CryptoCustomMessageTest.java index c1303e3..bf0fa6d 100644 --- a/extensions/src/test/java/ch/dissem/bitmessage/extensions/CryptoCustomMessageTest.java +++ b/extensions/src/test/java/ch/dissem/bitmessage/extensions/CryptoCustomMessageTest.java @@ -30,7 +30,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; import static org.junit.Assert.assertEquals; public class CryptoCustomMessageTest extends TestBase { @@ -39,9 +39,9 @@ public class CryptoCustomMessageTest extends TestBase { PrivateKey privateKey = PrivateKey.read(TestUtils.getResource("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey")); BitmessageAddress sendingIdentity = new BitmessageAddress(privateKey); - GenericPayload payloadBefore = new GenericPayload(0, 1, security().randomBytes(100)); + GenericPayload payloadBefore = new GenericPayload(0, 1, cryptography().randomBytes(100)); CryptoCustomMessage<GenericPayload> messageBefore = new CryptoCustomMessage<>(payloadBefore); - messageBefore.signAndEncrypt(sendingIdentity, security().createPublicKey(sendingIdentity.getPublicDecryptionKey())); + messageBefore.signAndEncrypt(sendingIdentity, cryptography().createPublicKey(sendingIdentity.getPublicDecryptionKey())); ByteArrayOutputStream out = new ByteArrayOutputStream(); messageBefore.write(out); @@ -52,7 +52,7 @@ public class CryptoCustomMessageTest extends TestBase { new CryptoCustomMessage.Reader<GenericPayload>() { @Override public GenericPayload read(BitmessageAddress ignore, InputStream in) throws IOException { - return GenericPayload.read(0, in, 1, 100); + return GenericPayload.read(0, 1, in, 100); } }); GenericPayload payloadAfter = messageAfter.decrypt(sendingIdentity.getPublicDecryptionKey()); @@ -65,11 +65,11 @@ public class CryptoCustomMessageTest extends TestBase { PrivateKey privateKey = PrivateKey.read(TestUtils.getResource("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8.privkey")); final BitmessageAddress sendingIdentity = new BitmessageAddress(privateKey); - ProofOfWorkRequest requestBefore = new ProofOfWorkRequest(sendingIdentity, security().randomBytes(64), + ProofOfWorkRequest requestBefore = new ProofOfWorkRequest(sendingIdentity, cryptography().randomBytes(64), ProofOfWorkRequest.Request.CALCULATE); CryptoCustomMessage<ProofOfWorkRequest> messageBefore = new CryptoCustomMessage<>(requestBefore); - messageBefore.signAndEncrypt(sendingIdentity, security().createPublicKey(sendingIdentity.getPublicDecryptionKey())); + messageBefore.signAndEncrypt(sendingIdentity, cryptography().createPublicKey(sendingIdentity.getPublicDecryptionKey())); ByteArrayOutputStream out = new ByteArrayOutputStream(); diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 3d0dee6..05ef575 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 94f382d..ebcdff0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Mar 16 20:59:34 CET 2015 +#Fri Aug 12 11:52:04 CEST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/gradlew b/gradlew index 91a7e26..9d82f78 100755 --- a/gradlew +++ b/gradlew @@ -42,11 +42,6 @@ case "`uname`" in ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" @@ -61,9 +56,9 @@ while [ -h "$PRG" ] ; do fi done SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- +cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" -cd "$SAVED" >&- +cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -114,6 +109,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` diff --git a/networking/build.gradle b/networking/build.gradle index 984f585..49cafa5 100644 --- a/networking/build.gradle +++ b/networking/build.gradle @@ -12,7 +12,7 @@ uploadArchives { dependencies { compile project(':core') - testCompile 'junit:junit:4.11' + testCompile 'junit:junit:4.12' testCompile 'org.slf4j:slf4j-simple:1.7.12' testCompile 'org.mockito:mockito-core:1.10.19' testCompile project(path: ':core', configuration: 'testArtifacts') diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/AbstractConnection.java b/networking/src/main/java/ch/dissem/bitmessage/networking/AbstractConnection.java new file mode 100644 index 0000000..73dce63 --- /dev/null +++ b/networking/src/main/java/ch/dissem/bitmessage/networking/AbstractConnection.java @@ -0,0 +1,343 @@ +/* + * Copyright 2016 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.networking; + +import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.entity.*; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException; +import ch.dissem.bitmessage.exception.NodeException; +import ch.dissem.bitmessage.ports.NetworkHandler; +import ch.dissem.bitmessage.utils.UnixTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; + +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.networking.AbstractConnection.Mode.SERVER; +import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.SYNC; +import static ch.dissem.bitmessage.networking.AbstractConnection.State.*; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; +import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; + +/** + * Contains everything used by both the old streams-oriented NetworkHandler and the new NioNetworkHandler, + * respectively their connection objects. + */ +public abstract class AbstractConnection { + private static final Logger LOG = LoggerFactory.getLogger(AbstractConnection.class); + protected final InternalContext ctx; + protected final Mode mode; + protected final NetworkAddress host; + protected final NetworkAddress node; + protected final NetworkHandler.MessageListener listener; + protected final Map<InventoryVector, Long> ivCache; + protected final Deque<MessagePayload> sendingQueue; + protected final Set<InventoryVector> commonRequestedObjects; + protected final Set<InventoryVector> requestedObjects; + + protected volatile State state; + protected long lastObjectTime; + + private final long syncTimeout; + private long syncReadTimeout = Long.MAX_VALUE; + + protected long peerNonce; + protected int version; + protected long[] streams; + private boolean verackSent; + private boolean verackReceived; + + public AbstractConnection(InternalContext context, Mode mode, + NetworkAddress node, + Set<InventoryVector> commonRequestedObjects, + long syncTimeout) { + this.ctx = context; + this.mode = mode; + this.host = new NetworkAddress.Builder().ipv6(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0).port(0).build(); + this.node = node; + this.listener = context.getNetworkListener(); + this.syncTimeout = (syncTimeout > 0 ? UnixTime.now(+syncTimeout) : 0); + this.requestedObjects = Collections.newSetFromMap(new ConcurrentHashMap<InventoryVector, Boolean>(10_000)); + this.ivCache = new ConcurrentHashMap<>(); + this.sendingQueue = new ConcurrentLinkedDeque<>(); + this.state = CONNECTING; + this.commonRequestedObjects = commonRequestedObjects; + } + + public Mode getMode() { + return mode; + } + + public NetworkAddress getNode() { + return node; + } + + public State getState() { + return state; + } + + public long[] getStreams() { + return streams; + } + + protected void handleMessage(MessagePayload payload) { + switch (state) { + case ACTIVE: + receiveMessage(payload); + break; + + case DISCONNECTED: + break; + + default: + handleCommand(payload); + break; + } + } + + private void receiveMessage(MessagePayload messagePayload) { + switch (messagePayload.getCommand()) { + case INV: + receiveMessage((Inv) messagePayload); + break; + case GETDATA: + receiveMessage((GetData) messagePayload); + break; + case OBJECT: + receiveMessage((ObjectMessage) messagePayload); + break; + case ADDR: + receiveMessage((Addr) messagePayload); + break; + case CUSTOM: + case VERACK: + case VERSION: + default: + throw new IllegalStateException("Unexpectedly received '" + messagePayload.getCommand() + "' command"); + } + } + + private void receiveMessage(Inv inv) { + int originalSize = inv.getInventory().size(); + updateIvCache(inv.getInventory()); + List<InventoryVector> missing = ctx.getInventory().getMissing(inv.getInventory(), streams); + missing.removeAll(commonRequestedObjects); + LOG.trace("Received inventory with " + originalSize + " elements, of which are " + + missing.size() + " missing."); + send(new GetData.Builder().inventory(missing).build()); + } + + private void receiveMessage(GetData getData) { + for (InventoryVector iv : getData.getInventory()) { + ObjectMessage om = ctx.getInventory().getObject(iv); + if (om != null) sendingQueue.offer(om); + } + } + + private void receiveMessage(ObjectMessage objectMessage) { + requestedObjects.remove(objectMessage.getInventoryVector()); + if (ctx.getInventory().contains(objectMessage)) { + LOG.trace("Received object " + objectMessage.getInventoryVector() + " - already in inventory"); + return; + } + try { + listener.receive(objectMessage); + cryptography().checkProofOfWork(objectMessage, NETWORK_NONCE_TRIALS_PER_BYTE, NETWORK_EXTRA_BYTES); + ctx.getInventory().storeObject(objectMessage); + // offer object to some random nodes so it gets distributed throughout the network: + ctx.getNetworkHandler().offer(objectMessage.getInventoryVector()); + lastObjectTime = UnixTime.now(); + } catch (InsufficientProofOfWorkException e) { + LOG.warn(e.getMessage()); + // DebugUtils.saveToFile(objectMessage); // this line must not be committed active + } catch (IOException e) { + LOG.error("Stream " + objectMessage.getStream() + ", object type " + objectMessage.getType() + ": " + e.getMessage(), e); + } finally { + if (!commonRequestedObjects.remove(objectMessage.getInventoryVector())) { + LOG.debug("Received object that wasn't requested."); + } + } + } + + private void receiveMessage(Addr addr) { + LOG.trace("Received " + addr.getAddresses().size() + " addresses."); + ctx.getNodeRegistry().offerAddresses(addr.getAddresses()); + } + + private void updateIvCache(List<InventoryVector> inventory) { + cleanupIvCache(); + Long now = UnixTime.now(); + for (InventoryVector iv : inventory) { + ivCache.put(iv, now); + } + } + + public void offer(InventoryVector iv) { + sendingQueue.offer(new Inv.Builder() + .addInventoryVector(iv) + .build()); + updateIvCache(Collections.singletonList(iv)); + } + + public boolean knowsOf(InventoryVector iv) { + return ivCache.containsKey(iv); + } + + private void cleanupIvCache() { + Long fiveMinutesAgo = UnixTime.now(-5 * MINUTE); + for (Map.Entry<InventoryVector, Long> entry : ivCache.entrySet()) { + if (entry.getValue() < fiveMinutesAgo) { + ivCache.remove(entry.getKey()); + } + } + } + + private void handleCommand(MessagePayload payload) { + switch (payload.getCommand()) { + case VERSION: + handleVersion((Version) payload); + break; + case VERACK: + if (verackSent) { + activateConnection(); + } + verackReceived = true; + break; + case CUSTOM: + MessagePayload response = ctx.getCustomCommandHandler().handle((CustomMessage) payload); + if (response == null) { + disconnect(); + } else { + send(response); + } + break; + default: + throw new NodeException("Command 'version' or 'verack' expected, but was '" + + payload.getCommand() + "'"); + } + } + + private void activateConnection() { + LOG.info("Successfully established connection with node " + node); + state = ACTIVE; + node.setTime(UnixTime.now()); + if (mode != SYNC) { + sendAddresses(); + ctx.getNodeRegistry().offerAddresses(Collections.singletonList(node)); + } + sendInventory(); + } + + private void sendAddresses() { + List<NetworkAddress> addresses = ctx.getNodeRegistry().getKnownAddresses(1000, streams); + sendingQueue.offer(new Addr.Builder().addresses(addresses).build()); + } + + private void sendInventory() { + List<InventoryVector> inventory = ctx.getInventory().getInventory(streams); + for (int i = 0; i < inventory.size(); i += 50000) { + sendingQueue.offer(new Inv.Builder() + .inventory(inventory.subList(i, Math.min(inventory.size(), i + 50000))) + .build()); + } + } + + private void handleVersion(Version version) { + if (version.getNonce() == ctx.getClientNonce()) { + LOG.info("Tried to connect to self, disconnecting."); + disconnect(); + } else if (version.getVersion() >= BitmessageContext.CURRENT_VERSION) { + this.peerNonce = version.getNonce(); + if (peerNonce == ctx.getClientNonce()) disconnect(); + + this.version = version.getVersion(); + this.streams = version.getStreams(); + verackSent = true; + send(new VerAck()); + if (mode == SERVER) { + send(new Version.Builder().defaults(ctx.getClientNonce()).addrFrom(host).addrRecv(node).build()); + } + if (verackReceived) { + activateConnection(); + } + } else { + LOG.info("Received unsupported version " + version.getVersion() + ", disconnecting."); + disconnect(); + } + } + + @SuppressWarnings("RedundantIfStatement") + protected boolean syncFinished(NetworkMessage msg) { + if (mode != SYNC) { + return false; + } + if (Thread.interrupted()) { + return true; + } + if (state != ACTIVE) { + return false; + } + if (syncTimeout < UnixTime.now()) { + LOG.info("Synchronization timed out"); + return true; + } + if (!sendingQueue.isEmpty()) { + syncReadTimeout = System.currentTimeMillis() + 1000; + return false; + } + if (msg == null) { + return syncReadTimeout < System.currentTimeMillis(); + } else { + syncReadTimeout = System.currentTimeMillis() + 1000; + return false; + } + } + + public void disconnect() { + state = DISCONNECTED; + + // Make sure objects that are still missing are requested from other nodes + ctx.getNetworkHandler().request(requestedObjects); + } + + protected abstract void send(MessagePayload payload); + + public enum Mode {SERVER, CLIENT, SYNC} + + public enum State {CONNECTING, ACTIVE, DISCONNECTED} + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AbstractConnection that = (AbstractConnection) o; + return Objects.equals(node, that.node); + } + + @Override + public int hashCode() { + return Objects.hash(node); + } +} diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/Connection.java b/networking/src/main/java/ch/dissem/bitmessage/networking/Connection.java index 54bfee4..64772e5 100644 --- a/networking/src/main/java/ch/dissem/bitmessage/networking/Connection.java +++ b/networking/src/main/java/ch/dissem/bitmessage/networking/Connection.java @@ -16,13 +16,13 @@ package ch.dissem.bitmessage.networking; -import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.InternalContext; -import ch.dissem.bitmessage.entity.*; +import ch.dissem.bitmessage.entity.GetData; +import ch.dissem.bitmessage.entity.MessagePayload; +import ch.dissem.bitmessage.entity.NetworkMessage; +import ch.dissem.bitmessage.entity.Version; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; -import ch.dissem.bitmessage.exception.InsufficientProofOfWorkException; -import ch.dissem.bitmessage.exception.NodeException; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.ports.NetworkHandler.MessageListener; import ch.dissem.bitmessage.utils.UnixTime; @@ -36,86 +36,56 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketTimeoutException; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.ConcurrentMap; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; -import static ch.dissem.bitmessage.networking.Connection.Mode.CLIENT; -import static ch.dissem.bitmessage.networking.Connection.Mode.SYNC; -import static ch.dissem.bitmessage.networking.Connection.State.*; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.CLIENT; +import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.SYNC; +import static ch.dissem.bitmessage.networking.AbstractConnection.State.ACTIVE; +import static ch.dissem.bitmessage.networking.AbstractConnection.State.DISCONNECTED; import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; /** * A connection to a specific node */ -class Connection { +class Connection extends AbstractConnection { public static final int READ_TIMEOUT = 2000; private static final Logger LOG = LoggerFactory.getLogger(Connection.class); private static final int CONNECT_TIMEOUT = 5000; private final long startTime; - private final ConcurrentMap<InventoryVector, Long> ivCache; - private final InternalContext ctx; - private final Mode mode; private final Socket socket; - private final MessageListener listener; - private final NetworkAddress host; - private final NetworkAddress node; - private final Queue<MessagePayload> sendingQueue = new ConcurrentLinkedDeque<>(); - private final Set<InventoryVector> commonRequestedObjects; - private final Set<InventoryVector> requestedObjects; - private final long syncTimeout; private final ReaderRunnable reader = new ReaderRunnable(); private final WriterRunnable writer = new WriterRunnable(); - private final DefaultNetworkHandler networkHandler; - private volatile State state; private InputStream in; private OutputStream out; - private int version; - private long[] streams; - private int readTimeoutCounter; private boolean socketInitialized; - private long lastObjectTime; - public Connection(InternalContext context, Mode mode, Socket socket, MessageListener listener, + public Connection(InternalContext context, Mode mode, Socket socket, Set<InventoryVector> requestedObjectsMap) throws IOException { - this(context, mode, listener, socket, requestedObjectsMap, - Collections.newSetFromMap(new ConcurrentHashMap<InventoryVector, Boolean>(10_000)), + this(context, mode, socket, requestedObjectsMap, new NetworkAddress.Builder().ip(socket.getInetAddress()).port(socket.getPort()).stream(1).build(), 0); } - public Connection(InternalContext context, Mode mode, NetworkAddress node, MessageListener listener, + public Connection(InternalContext context, Mode mode, NetworkAddress node, Set<InventoryVector> requestedObjectsMap) { - this(context, mode, listener, new Socket(), requestedObjectsMap, - Collections.newSetFromMap(new ConcurrentHashMap<InventoryVector, Boolean>(10_000)), + this(context, mode, new Socket(), requestedObjectsMap, node, 0); } - private Connection(InternalContext context, Mode mode, MessageListener listener, Socket socket, - Set<InventoryVector> commonRequestedObjects, Set<InventoryVector> requestedObjects, NetworkAddress node, long syncTimeout) { + private Connection(InternalContext context, Mode mode, Socket socket, + Set<InventoryVector> commonRequestedObjects, NetworkAddress node, long syncTimeout) { + super(context, mode, node, commonRequestedObjects, syncTimeout); this.startTime = UnixTime.now(); - this.ctx = context; - this.mode = mode; - this.state = CONNECTING; - this.listener = listener; this.socket = socket; - this.commonRequestedObjects = commonRequestedObjects; - this.requestedObjects = requestedObjects; - this.host = new NetworkAddress.Builder().ipv6(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0).port(0).build(); - this.node = node; - this.syncTimeout = (syncTimeout > 0 ? UnixTime.now(+syncTimeout) : 0); - this.ivCache = new ConcurrentHashMap<>(); - this.networkHandler = (DefaultNetworkHandler) ctx.getNetworkHandler(); } public static Connection sync(InternalContext ctx, InetAddress address, int port, MessageListener listener, long timeoutInSeconds) throws IOException { - return new Connection(ctx, Mode.SYNC, listener, new Socket(address, port), - new HashSet<InventoryVector>(), + return new Connection(ctx, SYNC, new Socket(address, port), new HashSet<InventoryVector>(), new NetworkAddress.Builder().ip(address).port(port).stream(1).build(), timeoutInSeconds); @@ -137,147 +107,8 @@ class Connection { return node; } - @SuppressWarnings("RedundantIfStatement") - private boolean syncFinished(NetworkMessage msg) { - if (mode != SYNC) { - return false; - } - if (Thread.interrupted()) { - return true; - } - if (state != ACTIVE) { - return false; - } - if (syncTimeout < UnixTime.now()) { - LOG.info("Synchronization timed out"); - return true; - } - if (msg == null) { - if (requestedObjects.isEmpty() && sendingQueue.isEmpty()) - return true; - - readTimeoutCounter++; - return readTimeoutCounter > 1; - } else { - readTimeoutCounter = 0; - return false; - } - } - - private void activateConnection() { - LOG.info("Successfully established connection with node " + node); - state = ACTIVE; - if (mode != SYNC) { - sendAddresses(); - ctx.getNodeRegistry().offerAddresses(Collections.singletonList(node)); - } - sendInventory(); - node.setTime(UnixTime.now()); - } - - private void cleanupIvCache() { - Long fiveMinutesAgo = UnixTime.now(-5 * MINUTE); - for (Map.Entry<InventoryVector, Long> entry : ivCache.entrySet()) { - if (entry.getValue() < fiveMinutesAgo) { - ivCache.remove(entry.getKey()); - } - } - } - - private void updateIvCache(InventoryVector... inventory) { - cleanupIvCache(); - Long now = UnixTime.now(); - for (InventoryVector iv : inventory) { - ivCache.put(iv, now); - } - } - - private void updateIvCache(List<InventoryVector> inventory) { - cleanupIvCache(); - Long now = UnixTime.now(); - for (InventoryVector iv : inventory) { - ivCache.put(iv, now); - } - } - - private void receiveMessage(MessagePayload messagePayload) { - switch (messagePayload.getCommand()) { - case INV: - Inv inv = (Inv) messagePayload; - int originalSize = inv.getInventory().size(); - updateIvCache(inv.getInventory()); - List<InventoryVector> missing = ctx.getInventory().getMissing(inv.getInventory(), streams); - missing.removeAll(commonRequestedObjects); - LOG.debug("Received inventory with " + originalSize + " elements, of which are " - + missing.size() + " missing."); - send(new GetData.Builder().inventory(missing).build()); - break; - case GETDATA: - GetData getData = (GetData) messagePayload; - for (InventoryVector iv : getData.getInventory()) { - ObjectMessage om = ctx.getInventory().getObject(iv); - if (om != null) sendingQueue.offer(om); - } - break; - case OBJECT: - ObjectMessage objectMessage = (ObjectMessage) messagePayload; - try { - requestedObjects.remove(objectMessage.getInventoryVector()); - if (ctx.getInventory().contains(objectMessage)) { - LOG.trace("Received object " + objectMessage.getInventoryVector() + " - already in inventory"); - break; - } - listener.receive(objectMessage); - security().checkProofOfWork(objectMessage, ctx.getNetworkNonceTrialsPerByte(), ctx.getNetworkExtraBytes()); - ctx.getInventory().storeObject(objectMessage); - // offer object to some random nodes so it gets distributed throughout the network: - networkHandler.offer(objectMessage.getInventoryVector()); - lastObjectTime = UnixTime.now(); - } catch (InsufficientProofOfWorkException e) { - LOG.warn(e.getMessage()); - // DebugUtils.saveToFile(objectMessage); // this line must not be committed active - } catch (IOException e) { - LOG.error("Stream " + objectMessage.getStream() + ", object type " + objectMessage.getType() + ": " + e.getMessage(), e); - } finally { - if (commonRequestedObjects.remove(objectMessage.getInventoryVector())) { - LOG.debug("Received object that wasn't requested."); - } - } - break; - case ADDR: - Addr addr = (Addr) messagePayload; - LOG.debug("Received " + addr.getAddresses().size() + " addresses."); - ctx.getNodeRegistry().offerAddresses(addr.getAddresses()); - break; - case CUSTOM: - case VERACK: - case VERSION: - throw new RuntimeException("Unexpectedly received '" + messagePayload.getCommand() + "' command"); - } - } - - private void sendAddresses() { - List<NetworkAddress> addresses = ctx.getNodeRegistry().getKnownAddresses(1000, streams); - sendingQueue.offer(new Addr.Builder().addresses(addresses).build()); - } - - private void sendInventory() { - List<InventoryVector> inventory = ctx.getInventory().getInventory(streams); - for (int i = 0; i < inventory.size(); i += 50000) { - sendingQueue.offer(new Inv.Builder() - .inventory(inventory.subList(i, Math.min(inventory.size(), i + 50000))) - .build()); - } - } - - public void disconnect() { - state = DISCONNECTED; - - // Make sure objects that are still missing are requested from other nodes - networkHandler.request(requestedObjects); - } - - void send(MessagePayload payload) { + @Override + protected void send(MessagePayload payload) { try { if (payload instanceof GetData) { requestedObjects.addAll(((GetData) payload).getInventory()); @@ -291,17 +122,6 @@ class Connection { } } - public void offer(InventoryVector iv) { - sendingQueue.offer(new Inv.Builder() - .addInventoryVector(iv) - .build()); - updateIvCache(iv); - } - - public boolean knowsOf(InventoryVector iv) { - return ivCache.containsKey(iv); - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -336,18 +156,13 @@ class Connection { return writer; } - public enum Mode {SERVER, CLIENT, SYNC} - - public enum State {CONNECTING, ACTIVE, DISCONNECTED} - public class ReaderRunnable implements Runnable { @Override public void run() { - lastObjectTime = 0; try (Socket socket = Connection.this.socket) { initSocket(socket); if (mode == CLIENT || mode == SYNC) { - send(new Version.Builder().defaults().addrFrom(host).addrRecv(node).build()); + send(new Version.Builder().defaults(ctx.getClientNonce()).addrFrom(host).addrRecv(node).build()); } while (state != DISCONNECTED) { if (mode != SYNC) { @@ -357,74 +172,10 @@ class Connection { Thread.sleep(100); } } - try { - NetworkMessage msg = Factory.getNetworkMessage(version, in); - if (msg == null) - continue; - switch (state) { - case ACTIVE: - receiveMessage(msg.getPayload()); - break; - - default: - switch (msg.getPayload().getCommand()) { - case VERSION: - Version payload = (Version) msg.getPayload(); - if (payload.getNonce() == ctx.getClientNonce()) { - LOG.info("Tried to connect to self, disconnecting."); - disconnect(); - } else if (payload.getVersion() >= BitmessageContext.CURRENT_VERSION) { - version = payload.getVersion(); - streams = payload.getStreams(); - send(new VerAck()); - switch (mode) { - case SERVER: - send(new Version.Builder().defaults().addrFrom(host).addrRecv(node).build()); - break; - case CLIENT: - case SYNC: - activateConnection(); - break; - } - } else { - LOG.info("Received unsupported version " + payload.getVersion() + ", disconnecting."); - disconnect(); - } - break; - case VERACK: - switch (mode) { - case SERVER: - activateConnection(); - break; - case CLIENT: - case SYNC: - // NO OP - break; - } - break; - case CUSTOM: - MessagePayload response = ctx.getCustomCommandHandler().handle((CustomMessage) msg.getPayload()); - if (response != null) { - send(response); - } - disconnect(); - break; - default: - throw new NodeException("Command 'version' or 'verack' expected, but was '" - + msg.getPayload().getCommand() + "'"); - } - } - if (socket.isClosed() || syncFinished(msg) || checkOpenRequests()) disconnect(); - } catch (SocketTimeoutException ignore) { - if (state == ACTIVE) { - if (syncFinished(null)) disconnect(); - } - } + receive(); } - } catch (InterruptedException | IOException | NodeException e) { + } catch (Exception e) { LOG.trace("Reader disconnected from node " + node + ": " + e.getMessage()); - } catch (RuntimeException e) { - LOG.trace("Reader disconnecting from node " + node + " due to error: " + e.getMessage(), e); } finally { disconnect(); try { @@ -434,6 +185,19 @@ class Connection { } } } + + private void receive() throws InterruptedException { + try { + NetworkMessage msg = Factory.getNetworkMessage(version, in); + if (msg == null) + return; + handleMessage(msg.getPayload()); + if (socket.isClosed() || syncFinished(msg) || checkOpenRequests()) disconnect(); + } catch (SocketTimeoutException ignore) { + if (state == ACTIVE && syncFinished(null)) disconnect(); + } + } + } private boolean checkOpenRequests() { @@ -446,10 +210,10 @@ class Connection { try (Socket socket = Connection.this.socket) { initSocket(socket); while (state != DISCONNECTED) { - if (!sendingQueue.isEmpty()) { - send(sendingQueue.poll()); - } else { + if (sendingQueue.isEmpty()) { Thread.sleep(1000); + } else { + send(sendingQueue.poll()); } } } catch (IOException | InterruptedException e) { diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/ConnectionOrganizer.java b/networking/src/main/java/ch/dissem/bitmessage/networking/ConnectionOrganizer.java new file mode 100644 index 0000000..42021f6 --- /dev/null +++ b/networking/src/main/java/ch/dissem/bitmessage/networking/ConnectionOrganizer.java @@ -0,0 +1,119 @@ +/* + * Copyright 2016 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.networking; + +import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.ports.NetworkHandler; +import ch.dissem.bitmessage.utils.UnixTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Iterator; +import java.util.List; + +import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.CLIENT; +import static ch.dissem.bitmessage.networking.DefaultNetworkHandler.NETWORK_MAGIC_NUMBER; + +/** + * @author Christian Basler + */ +public class ConnectionOrganizer implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(ConnectionOrganizer.class); + + private final InternalContext ctx; + private final DefaultNetworkHandler networkHandler; + private final NetworkHandler.MessageListener listener; + + private Connection initialConnection; + + public ConnectionOrganizer(InternalContext ctx, + DefaultNetworkHandler networkHandler) { + this.ctx = ctx; + this.networkHandler = networkHandler; + this.listener = ctx.getNetworkListener(); + } + + @Override + public void run() { + try { + while (networkHandler.isRunning()) { + try { + int active = 0; + long now = UnixTime.now(); + + int diff = networkHandler.connections.size() - ctx.getConnectionLimit(); + if (diff > 0) { + for (Connection c : networkHandler.connections) { + c.disconnect(); + diff--; + if (diff == 0) break; + } + } + boolean forcedDisconnect = false; + for (Iterator<Connection> iterator = networkHandler.connections.iterator(); iterator.hasNext(); ) { + Connection c = iterator.next(); + // Just in case they were all created at the same time, don't disconnect + // all at once. + if (!forcedDisconnect && now - c.getStartTime() > ctx.getConnectionTTL()) { + c.disconnect(); + forcedDisconnect = true; + } + switch (c.getState()) { + case DISCONNECTED: + iterator.remove(); + break; + case ACTIVE: + active++; + break; + default: + // nothing to do + } + } + + if (active < NETWORK_MAGIC_NUMBER) { + List<NetworkAddress> addresses = ctx.getNodeRegistry().getKnownAddresses( + NETWORK_MAGIC_NUMBER - active, ctx.getStreams()); + boolean first = active == 0 && initialConnection == null; + for (NetworkAddress address : addresses) { + Connection c = new Connection(ctx, CLIENT, address, networkHandler.requestedObjects); + if (first) { + initialConnection = c; + first = false; + } + networkHandler.startConnection(c); + } + Thread.sleep(10000); + } else if (initialConnection == null) { + Thread.sleep(30000); + } else { + initialConnection.disconnect(); + initialConnection = null; + Thread.sleep(10000); + } + } catch (InterruptedException e) { + networkHandler.stop(); + } catch (Exception e) { + LOG.error("Error in connection manager. Ignored.", e); + } + } + } finally { + LOG.debug("Connection manager shutting down."); + networkHandler.stop(); + } + } +} diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/DefaultNetworkHandler.java b/networking/src/main/java/ch/dissem/bitmessage/networking/DefaultNetworkHandler.java index d3bec17..1af62b5 100644 --- a/networking/src/main/java/ch/dissem/bitmessage/networking/DefaultNetworkHandler.java +++ b/networking/src/main/java/ch/dissem/bitmessage/networking/DefaultNetworkHandler.java @@ -22,55 +22,44 @@ import ch.dissem.bitmessage.entity.CustomMessage; import ch.dissem.bitmessage.entity.GetData; import ch.dissem.bitmessage.entity.NetworkMessage; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; -import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.exception.NodeException; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.ports.NetworkHandler; import ch.dissem.bitmessage.utils.Collections; import ch.dissem.bitmessage.utils.Property; -import ch.dissem.bitmessage.utils.UnixTime; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.InetAddress; -import java.net.ServerSocket; import java.net.Socket; import java.util.*; import java.util.concurrent.*; -import static ch.dissem.bitmessage.networking.Connection.Mode.CLIENT; -import static ch.dissem.bitmessage.networking.Connection.Mode.SERVER; -import static ch.dissem.bitmessage.networking.Connection.State.ACTIVE; +import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.SERVER; +import static ch.dissem.bitmessage.networking.AbstractConnection.State.ACTIVE; import static ch.dissem.bitmessage.utils.DebugUtils.inc; +import static ch.dissem.bitmessage.utils.ThreadFactoryBuilder.pool; import static java.util.Collections.newSetFromMap; /** * Handles all the networky stuff. + * + * @deprecated use {@link ch.dissem.bitmessage.networking.nio.NioNetworkHandler NioNetworkHandler} instead. */ +@Deprecated public class DefaultNetworkHandler implements NetworkHandler, ContextHolder { - private final static Logger LOG = LoggerFactory.getLogger(DefaultNetworkHandler.class); - public final static int NETWORK_MAGIC_NUMBER = 8; - - private final Collection<Connection> connections = new ConcurrentLinkedQueue<>(); - private final ExecutorService pool; + final Collection<Connection> connections = new ConcurrentLinkedQueue<>(); + private final ExecutorService pool = Executors.newCachedThreadPool( + pool("network") + .lowPrio() + .daemon() + .build()); private InternalContext ctx; - private ServerSocket serverSocket; + private ServerRunnable server; private volatile boolean running; - private Set<InventoryVector> requestedObjects = newSetFromMap(new ConcurrentHashMap<InventoryVector, Boolean>(50_000)); - - public DefaultNetworkHandler() { - pool = Executors.newCachedThreadPool(new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - Thread thread = Executors.defaultThreadFactory().newThread(r); - thread.setPriority(Thread.MIN_PRIORITY); - return thread; - } - }); - } + final Set<InventoryVector> requestedObjects = newSetFromMap(new ConcurrentHashMap<InventoryVector, Boolean>(50_000)); @Override public void setContext(InternalContext context) { @@ -78,14 +67,14 @@ public class DefaultNetworkHandler implements NetworkHandler, ContextHolder { } @Override - public Future<?> synchronize(InetAddress server, int port, MessageListener listener, long timeoutInSeconds) { + public Future<?> synchronize(InetAddress server, int port, long timeoutInSeconds) { try { - Connection connection = Connection.sync(ctx, server, port, listener, timeoutInSeconds); + Connection connection = Connection.sync(ctx, server, port, ctx.getNetworkListener(), timeoutInSeconds); Future<?> reader = pool.submit(connection.getReader()); pool.execute(connection.getWriter()); return reader; } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -102,112 +91,27 @@ public class DefaultNetworkHandler implements NetworkHandler, ContextHolder { throw new NodeException("No response from node " + server); } else { throw new NodeException("Unexpected response from node " + - server + ": " + networkMessage.getPayload().getCommand()); + server + ": " + networkMessage.getPayload().getCommand()); } } } catch (IOException e) { - throw new RuntimeException(e); + throw new NodeException(e.getMessage(), e); } } @Override - public void start(final MessageListener listener) { - if (listener == null) { - throw new IllegalStateException("Listener must be set at start"); - } + public void start() { if (running) { throw new IllegalStateException("Network already running - you need to stop first."); } try { running = true; connections.clear(); - serverSocket = new ServerSocket(ctx.getPort()); - pool.execute(new Runnable() { - @Override - public void run() { - while (!serverSocket.isClosed()) { - try { - Socket socket = serverSocket.accept(); - socket.setSoTimeout(Connection.READ_TIMEOUT); - startConnection(new Connection(ctx, SERVER, socket, listener, requestedObjects)); - } catch (IOException e) { - LOG.debug(e.getMessage(), e); - } - } - } - }); - pool.execute(new Runnable() { - public Connection initialConnection; - - @Override - public void run() { - try { - while (running) { - try { - int active = 0; - long now = UnixTime.now(); - synchronized (connections) { - int diff = connections.size() - ctx.getConnectionLimit(); - if (diff > 0) { - for (Connection c : connections) { - c.disconnect(); - diff--; - if (diff == 0) break; - } - } - boolean forcedDisconnect = false; - for (Iterator<Connection> iterator = connections.iterator(); iterator.hasNext(); ) { - Connection c = iterator.next(); - // Just in case they were all created at the same time, don't disconnect - // all at once. - if (!forcedDisconnect && now - c.getStartTime() > ctx.getConnectionTTL()) { - c.disconnect(); - forcedDisconnect = true; - } - switch (c.getState()) { - case DISCONNECTED: - iterator.remove(); - break; - case ACTIVE: - active++; - break; - } - } - } - if (active < NETWORK_MAGIC_NUMBER) { - List<NetworkAddress> addresses = ctx.getNodeRegistry().getKnownAddresses( - NETWORK_MAGIC_NUMBER - active, ctx.getStreams()); - boolean first = active == 0 && initialConnection == null; - for (NetworkAddress address : addresses) { - Connection c = new Connection(ctx, CLIENT, address, listener, requestedObjects); - if (first) { - initialConnection = c; - first = false; - } - startConnection(c); - } - Thread.sleep(10000); - } else if (initialConnection != null) { - initialConnection.disconnect(); - initialConnection = null; - Thread.sleep(10000); - } else { - Thread.sleep(30000); - } - } catch (InterruptedException e) { - running = false; - } catch (Exception e) { - LOG.error("Error in connection manager. Ignored.", e); - } - } - } finally { - LOG.debug("Connection manager shutting down."); - running = false; - } - } - }); + server = new ServerRunnable(ctx, this); + pool.execute(server); + pool.execute(new ConnectionOrganizer(ctx, this)); } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -218,13 +122,9 @@ public class DefaultNetworkHandler implements NetworkHandler, ContextHolder { @Override public void stop() { - running = false; - try { - serverSocket.close(); - } catch (IOException e) { - LOG.debug(e.getMessage(), e); - } + server.close(); synchronized (connections) { + running = false; for (Connection c : connections) { c.disconnect(); } @@ -232,8 +132,12 @@ public class DefaultNetworkHandler implements NetworkHandler, ContextHolder { requestedObjects.clear(); } - private void startConnection(Connection c) { + void startConnection(Connection c) { + if (!running) return; + synchronized (connections) { + if (!running) return; + // prevent connecting twice to the same node if (connections.contains(c)) { return; @@ -247,11 +151,9 @@ public class DefaultNetworkHandler implements NetworkHandler, ContextHolder { @Override public void offer(final InventoryVector iv) { List<Connection> target = new LinkedList<>(); - synchronized (connections) { - for (Connection connection : connections) { - if (connection.getState() == ACTIVE && !connection.knowsOf(iv)) { - target.add(connection); - } + for (Connection connection : connections) { + if (connection.getState() == ACTIVE && !connection.knowsOf(iv)) { + target.add(connection); } } List<Connection> randomSubset = Collections.selectRandom(NETWORK_MAGIC_NUMBER, target); @@ -266,10 +168,9 @@ public class DefaultNetworkHandler implements NetworkHandler, ContextHolder { TreeMap<Long, Integer> incomingConnections = new TreeMap<>(); TreeMap<Long, Integer> outgoingConnections = new TreeMap<>(); - synchronized (connections) { - for (Connection connection : connections) { - if (connection.getState() == ACTIVE) { - long stream = connection.getNode().getStream(); + for (Connection connection : connections) { + if (connection.getState() == ACTIVE) { + for (long stream : connection.getStreams()) { streams.add(stream); if (connection.getMode() == SERVER) { inc(incomingConnections, stream); @@ -285,68 +186,63 @@ public class DefaultNetworkHandler implements NetworkHandler, ContextHolder { int incoming = incomingConnections.containsKey(stream) ? incomingConnections.get(stream) : 0; int outgoing = outgoingConnections.containsKey(stream) ? outgoingConnections.get(stream) : 0; streamProperties[i] = new Property("stream " + stream, - null, new Property("nodes", incoming + outgoing), - new Property("incoming", incoming), - new Property("outgoing", outgoing) + null, new Property("nodes", incoming + outgoing), + new Property("incoming", incoming), + new Property("outgoing", outgoing) ); i++; } return new Property("network", null, - new Property("connectionManager", running ? "running" : "stopped"), - new Property("connections", null, streamProperties), - new Property("requestedObjects", requestedObjects.size()) + new Property("connectionManager", running ? "running" : "stopped"), + new Property("connections", null, streamProperties), + new Property("requestedObjects", requestedObjects.size()) ); } - void request(Set<InventoryVector> inventoryVectors) { + @Override + public void request(Collection<InventoryVector> inventoryVectors) { if (!running || inventoryVectors.isEmpty()) return; - synchronized (connections) { - Map<Connection, List<InventoryVector>> distribution = new HashMap<>(); - for (Connection connection : connections) { - if (connection.getState() == ACTIVE) { - distribution.put(connection, new LinkedList<InventoryVector>()); - } - } - Iterator<InventoryVector> iterator = inventoryVectors.iterator(); - InventoryVector next; - if (iterator.hasNext()) { - next = iterator.next(); - } else { - return; - } - boolean firstRound = true; - while (firstRound || iterator.hasNext()) { - if (!firstRound) { - next = iterator.next(); - firstRound = true; - } else { - firstRound = false; - } - for (Connection connection : distribution.keySet()) { - if (connection.knowsOf(next)) { - List<InventoryVector> ivs = distribution.get(connection); - if (ivs.size() == 50_000) { - connection.send(new GetData.Builder().inventory(ivs).build()); - ivs.clear(); - } - ivs.add(next); - iterator.remove(); - if (iterator.hasNext()) { - next = iterator.next(); - firstRound = true; - } else { - firstRound = false; - break; - } + Map<Connection, List<InventoryVector>> distribution = new HashMap<>(); + for (Connection connection : connections) { + if (connection.getState() == ACTIVE) { + distribution.put(connection, new LinkedList<InventoryVector>()); + } + } + Iterator<InventoryVector> iterator = inventoryVectors.iterator(); + if (!iterator.hasNext()) { + return; + } + InventoryVector next = iterator.next(); + Connection previous = null; + do { + for (Connection connection : distribution.keySet()) { + if (connection == previous) { + next = iterator.next(); + } + if (connection.knowsOf(next)) { + List<InventoryVector> ivs = distribution.get(connection); + if (ivs.size() == GetData.MAX_INVENTORY_SIZE) { + connection.send(new GetData.Builder().inventory(ivs).build()); + ivs.clear(); + } + ivs.add(next); + iterator.remove(); + + if (iterator.hasNext()) { + next = iterator.next(); + previous = connection; + } else { + break; } } } - for (Connection connection : distribution.keySet()) { - List<InventoryVector> ivs = distribution.get(connection); - if (!ivs.isEmpty()) { - connection.send(new GetData.Builder().inventory(ivs).build()); - } + } while (iterator.hasNext()); + + for (Connection connection : distribution.keySet()) { + List<InventoryVector> ivs = distribution.get(connection); + if (!ivs.isEmpty()) { + connection.send(new GetData.Builder().inventory(ivs).build()); } } } diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/ServerRunnable.java b/networking/src/main/java/ch/dissem/bitmessage/networking/ServerRunnable.java new file mode 100644 index 0000000..3c67b95 --- /dev/null +++ b/networking/src/main/java/ch/dissem/bitmessage/networking/ServerRunnable.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016 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.networking; + +import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.ports.NetworkHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; + +import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.SERVER; + +/** + * @author Christian Basler + */ +public class ServerRunnable implements Runnable, Closeable { + private static final Logger LOG = LoggerFactory.getLogger(ServerRunnable.class); + private final InternalContext ctx; + private final ServerSocket serverSocket; + private final DefaultNetworkHandler networkHandler; + private final NetworkHandler.MessageListener listener; + + public ServerRunnable(InternalContext ctx, DefaultNetworkHandler networkHandler) throws IOException { + this.ctx = ctx; + this.networkHandler = networkHandler; + this.listener = ctx.getNetworkListener(); + this.serverSocket = new ServerSocket(ctx.getPort()); + } + + @Override + public void run() { + while (!serverSocket.isClosed()) { + try { + Socket socket = serverSocket.accept(); + socket.setSoTimeout(Connection.READ_TIMEOUT); + networkHandler.startConnection(new Connection(ctx, SERVER, socket, networkHandler.requestedObjects)); + } catch (IOException e) { + LOG.debug(e.getMessage(), e); + } + } + } + + @Override + public void close() { + try { + serverSocket.close(); + } catch (IOException e) { + LOG.debug(e.getMessage(), e); + } + } +} diff --git a/networking/src/main/java/ch/dissem/bitmessage/networking/nio/ConnectionInfo.java b/networking/src/main/java/ch/dissem/bitmessage/networking/nio/ConnectionInfo.java new file mode 100644 index 0000000..2e24883 --- /dev/null +++ b/networking/src/main/java/ch/dissem/bitmessage/networking/nio/ConnectionInfo.java @@ -0,0 +1,160 @@ +/* + * Copyright 2016 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.networking.nio; + +import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.entity.GetData; +import ch.dissem.bitmessage.entity.MessagePayload; +import ch.dissem.bitmessage.entity.NetworkMessage; +import ch.dissem.bitmessage.entity.Version; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.exception.NodeException; +import ch.dissem.bitmessage.factory.V3MessageReader; +import ch.dissem.bitmessage.networking.AbstractConnection; + +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.Queue; +import java.util.Set; + +import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.CLIENT; +import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.SYNC; + +/** + * Represents the current state of a connection. + */ +public class ConnectionInfo extends AbstractConnection { + private final ByteBuffer headerOut = ByteBuffer.allocate(24); + private ByteBuffer payloadOut; + private V3MessageReader reader = new V3MessageReader(); + private boolean syncFinished; + private long lastUpdate = System.currentTimeMillis(); + + public ConnectionInfo(InternalContext context, Mode mode, NetworkAddress node, + Set<InventoryVector> commonRequestedObjects, long syncTimeout) { + super(context, mode, node, commonRequestedObjects, syncTimeout); + headerOut.flip(); + if (mode == CLIENT || mode == SYNC) { + send(new Version.Builder().defaults(ctx.getClientNonce()).addrFrom(host).addrRecv(node).build()); + } + } + + public State getState() { + return state; + } + + public boolean knowsOf(InventoryVector iv) { + return ivCache.containsKey(iv); + } + + public Queue<MessagePayload> getSendingQueue() { + return sendingQueue; + } + + public ByteBuffer getInBuffer() { + if (reader == null) { + throw new NodeException("Node is disconnected"); + } + return reader.getActiveBuffer(); + } + + public void updateWriter() { + if (!headerOut.hasRemaining() && !sendingQueue.isEmpty()) { + headerOut.clear(); + MessagePayload payload = sendingQueue.poll(); + payloadOut = new NetworkMessage(payload).writeHeaderAndGetPayloadBuffer(headerOut); + headerOut.flip(); + lastUpdate = System.currentTimeMillis(); + } + } + + public ByteBuffer[] getOutBuffers() { + return new ByteBuffer[]{headerOut, payloadOut}; + } + + public void cleanupBuffers() { + if (payloadOut != null && !payloadOut.hasRemaining()) { + payloadOut = null; + } + } + + public void updateReader() { + reader.update(); + if (!reader.getMessages().isEmpty()) { + Iterator<NetworkMessage> iterator = reader.getMessages().iterator(); + NetworkMessage msg = null; + while (iterator.hasNext()) { + msg = iterator.next(); + handleMessage(msg.getPayload()); + iterator.remove(); + } + syncFinished = syncFinished(msg); + } + lastUpdate = System.currentTimeMillis(); + } + + public void updateSyncStatus() { + if (!syncFinished) { + syncFinished = (reader == null || reader.getMessages().isEmpty()) && syncFinished(null); + } + } + + public boolean isExpired() { + switch (state) { + case CONNECTING: + // the TCP timeout starts out at 20 seconds + return lastUpdate < System.currentTimeMillis() - 20_000; + case ACTIVE: + // after verack messages are exchanged, the timeout is raised to 10 minutes + return lastUpdate < System.currentTimeMillis() - 600_000; + case DISCONNECTED: + return true; + default: + throw new IllegalStateException("Unknown state: " + state); + } + } + + @Override + public void disconnect() { + super.disconnect(); + if (reader != null) { + reader.cleanup(); + reader = null; + } + payloadOut = null; + } + + public boolean isSyncFinished() { + return syncFinished; + } + + @Override + protected void send(MessagePayload payload) { + sendingQueue.add(payload); + if (payload instanceof GetData) { + requestedObjects.addAll(((GetData) payload).getInventory()); + commonRequestedObjects.addAll(((GetData) payload).getInventory()); + } + } + + public boolean isWritePending() { + return !sendingQueue.isEmpty() + || headerOut != null && headerOut.hasRemaining() + || payloadOut != null && payloadOut.hasRemaining(); + } +} 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 new file mode 100644 index 0000000..f9fa06a --- /dev/null +++ b/networking/src/main/java/ch/dissem/bitmessage/networking/nio/NioNetworkHandler.java @@ -0,0 +1,510 @@ +/* + * Copyright 2016 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.networking.nio; + +import ch.dissem.bitmessage.InternalContext; +import ch.dissem.bitmessage.entity.CustomMessage; +import ch.dissem.bitmessage.entity.GetData; +import ch.dissem.bitmessage.entity.NetworkMessage; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.exception.ApplicationException; +import ch.dissem.bitmessage.exception.NodeException; +import ch.dissem.bitmessage.factory.V3MessageReader; +import ch.dissem.bitmessage.ports.NetworkHandler; +import ch.dissem.bitmessage.utils.Property; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NoRouteToHostException; +import java.nio.ByteBuffer; +import java.nio.channels.*; +import java.util.*; +import java.util.concurrent.*; + +import static ch.dissem.bitmessage.networking.AbstractConnection.Mode.*; +import static ch.dissem.bitmessage.networking.AbstractConnection.State.ACTIVE; +import static ch.dissem.bitmessage.networking.AbstractConnection.State.DISCONNECTED; +import static ch.dissem.bitmessage.utils.Collections.selectRandom; +import static ch.dissem.bitmessage.utils.DebugUtils.inc; +import static ch.dissem.bitmessage.utils.ThreadFactoryBuilder.pool; +import static java.nio.channels.SelectionKey.*; +import static java.util.Collections.newSetFromMap; + +/** + * Network handler using java.nio, resulting in less threads. + */ +public class NioNetworkHandler implements NetworkHandler, InternalContext.ContextHolder { + private static final Logger LOG = LoggerFactory.getLogger(NioNetworkHandler.class); + private static final long REQUESTED_OBJECTS_MAX_TIME = 30 * 60_000; // 30 minutes + + private final ExecutorService threadPool = Executors.newCachedThreadPool( + pool("network") + .lowPrio() + .daemon() + .build()); + + private InternalContext ctx; + private Selector selector; + private ServerSocketChannel serverChannel; + private Queue<NetworkAddress> connectionQueue = new ConcurrentLinkedQueue<>(); + private Map<ConnectionInfo, SelectionKey> connections = new ConcurrentHashMap<>(); + private final Set<InventoryVector> requestedObjects = newSetFromMap(new ConcurrentHashMap<InventoryVector, Boolean>(10_000)); + private long requestedObjectsTimeout = 0; + + private Thread starter; + + @Override + public Future<Void> synchronize(final InetAddress server, final int port, final long timeoutInSeconds) { + return threadPool.submit(new Callable<Void>() { + @Override + public Void call() throws Exception { + try (SocketChannel channel = SocketChannel.open(new InetSocketAddress(server, port))) { + channel.configureBlocking(false); + ConnectionInfo connection = new ConnectionInfo(ctx, SYNC, + new NetworkAddress.Builder().ip(server).port(port).stream(1).build(), + new HashSet<InventoryVector>(), timeoutInSeconds); + while (channel.isConnected() && !connection.isSyncFinished()) { + write(channel, connection); + read(channel, connection); + Thread.sleep(10); + } + LOG.info("Synchronization finished"); + } + return null; + } + }); + } + + @Override + public CustomMessage send(InetAddress server, int port, CustomMessage request) { + try (SocketChannel channel = SocketChannel.open(new InetSocketAddress(server, port))) { + channel.configureBlocking(true); + ByteBuffer headerBuffer = ByteBuffer.allocate(HEADER_SIZE); + ByteBuffer payloadBuffer = new NetworkMessage(request).writeHeaderAndGetPayloadBuffer(headerBuffer); + headerBuffer.flip(); + while (headerBuffer.hasRemaining()) { + channel.write(headerBuffer); + } + while (payloadBuffer.hasRemaining()) { + channel.write(payloadBuffer); + } + + V3MessageReader reader = new V3MessageReader(); + while (channel.isConnected() && reader.getMessages().isEmpty()) { + if (channel.read(reader.getActiveBuffer()) > 0) { + reader.update(); + } else { + throw new NodeException("No response from node " + server); + } + } + NetworkMessage networkMessage; + if (reader.getMessages().isEmpty()) { + throw new NodeException("No response from node " + server); + } else { + networkMessage = reader.getMessages().get(0); + } + + if (networkMessage != null && networkMessage.getPayload() instanceof CustomMessage) { + return (CustomMessage) networkMessage.getPayload(); + } else { + if (networkMessage == null || networkMessage.getPayload() == null) { + throw new NodeException("Empty response from node " + server); + } else { + throw new NodeException("Unexpected response from node " + server + ": " + + networkMessage.getPayload().getClass()); + } + } + } catch (IOException e) { + throw new ApplicationException(e); + } + } + + @Override + public void start() { + if (selector != null && selector.isOpen()) { + throw new IllegalStateException("Network already running - you need to stop first."); + } + try { + selector = Selector.open(); + } catch (IOException e) { + throw new ApplicationException(e); + } + requestedObjectsTimeout = System.currentTimeMillis() + REQUESTED_OBJECTS_MAX_TIME; + requestedObjects.clear(); + + starter = thread("connection manager", new Runnable() { + @Override + public void run() { + while (selector.isOpen()) { + int missing = NETWORK_MAGIC_NUMBER; + for (ConnectionInfo connectionInfo : connections.keySet()) { + if (connectionInfo.getState() == ACTIVE) { + missing--; + if (missing == 0) break; + } + } + if (missing > 0) { + List<NetworkAddress> addresses = ctx.getNodeRegistry().getKnownAddresses(100, ctx.getStreams()); + addresses = selectRandom(missing, addresses); + for (NetworkAddress address : addresses) { + if (!isConnectedTo(address)) { + connectionQueue.offer(address); + } + } + } + + Iterator<Map.Entry<ConnectionInfo, SelectionKey>> it = connections.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry<ConnectionInfo, SelectionKey> e = it.next(); + if (!e.getValue().isValid() || e.getKey().isExpired()) { + try { + e.getValue().channel().close(); + } catch (Exception ignore) { + } + e.getValue().cancel(); + e.getValue().attach(null); + e.getKey().disconnect(); + it.remove(); + } + } + + // The list 'requested objects' helps to prevent downloading an object + // twice. From time to time there is an error though, and an object is + // never downloaded. To prevent a large list of failed objects and give + // them a chance to get downloaded again, let's clear the list from time + // to time. The timeout should be such that most of the initial object + // sync should be done by then, but small enough to prevent objects with + // a normal time out from not being downloaded at all. + long now = System.currentTimeMillis(); + if (now > requestedObjectsTimeout) { + requestedObjectsTimeout = now + REQUESTED_OBJECTS_MAX_TIME; + requestedObjects.clear(); + } + + try { + Thread.sleep(30_000); + } catch (InterruptedException e) { + return; + } + } + } + }); + + thread("selector worker", new Runnable() { + @Override + public void run() { + try { + serverChannel = ServerSocketChannel.open(); + serverChannel.configureBlocking(false); + serverChannel.socket().bind(new InetSocketAddress(ctx.getPort())); + serverChannel.register(selector, OP_ACCEPT, null); + + while (selector.isOpen()) { + selector.select(1000); + Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator(); + while (keyIterator.hasNext()) { + SelectionKey key = keyIterator.next(); + keyIterator.remove(); + if (key.attachment() == null) { + try { + if (key.isAcceptable()) { + // handle accept + try { + SocketChannel accepted = ((ServerSocketChannel) key.channel()).accept(); + accepted.configureBlocking(false); + ConnectionInfo connection = new ConnectionInfo(ctx, SERVER, + new NetworkAddress.Builder() + .ip(accepted.socket().getInetAddress()) + .port(accepted.socket().getPort()) + .stream(1) + .build(), + requestedObjects, 0 + ); + connections.put( + connection, + accepted.register(selector, OP_READ | OP_WRITE, connection) + ); + } catch (AsynchronousCloseException e) { + LOG.trace(e.getMessage()); + } catch (IOException e) { + LOG.error(e.getMessage(), e); + } + } + } catch (CancelledKeyException e) { + LOG.error(e.getMessage(), e); + } + } else { + // handle read/write + SocketChannel channel = (SocketChannel) key.channel(); + ConnectionInfo connection = (ConnectionInfo) key.attachment(); + try { + if (key.isConnectable()) { + if (!channel.finishConnect()) { + continue; + } + } + if (key.isWritable()) { + write(channel, connection); + } + if (key.isReadable()) { + read(channel, connection); + } + if (connection.getState() == DISCONNECTED) { + key.interestOps(0); + channel.close(); + } else if (connection.isWritePending()) { + key.interestOps(OP_READ | OP_WRITE); + } else { + key.interestOps(OP_READ); + } + } catch (CancelledKeyException | NodeException | IOException e) { + connection.disconnect(); + } + } + } + // 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); + } + } + // start new connections + if (!connectionQueue.isEmpty()) { + NetworkAddress address = connectionQueue.poll(); + try { + SocketChannel channel = SocketChannel.open(); + channel.configureBlocking(false); + channel.connect(new InetSocketAddress(address.toInetAddress(), address.getPort())); + ConnectionInfo connection = new ConnectionInfo(ctx, CLIENT, + address, + requestedObjects, 0 + ); + connections.put( + connection, + channel.register(selector, OP_CONNECT, connection) + ); + } catch (NoRouteToHostException ignore) { + // We'll try to connect to many offline nodes, so + // this is expected to happen quite a lot. + } catch (AsynchronousCloseException e) { + // The exception is expected if the network is being + // shut down, as we actually do asynchronously close + // the connections. + if (isRunning()) { + LOG.error(e.getMessage(), e); + } + } catch (IOException e) { + LOG.error(e.getMessage(), e); + } + } + } + selector.close(); + } catch (ClosedSelectorException ignore) { + } catch (IOException e) { + throw new ApplicationException(e); + } + } + }); + } + + private static void write(SocketChannel channel, ConnectionInfo connection) + throws IOException { + writeBuffer(connection.getOutBuffers(), channel); + + connection.updateWriter(); + + writeBuffer(connection.getOutBuffers(), channel); + connection.cleanupBuffers(); + } + + private static void writeBuffer(ByteBuffer[] buffers, SocketChannel channel) throws IOException { + if (buffers[1] == null) { + if (buffers[0].hasRemaining()) { + channel.write(buffers[0]); + } + } else if (buffers[1].hasRemaining() || buffers[0].hasRemaining()) { + channel.write(buffers); + } + } + + private static void read(SocketChannel channel, ConnectionInfo connection) throws IOException { + if (channel.read(connection.getInBuffer()) > 0) { + connection.updateReader(); + } + connection.updateSyncStatus(); + } + + private Thread thread(String threadName, Runnable runnable) { + Thread thread = new Thread(runnable, threadName); + thread.setDaemon(true); + thread.setPriority(Thread.MIN_PRIORITY); + thread.start(); + return thread; + } + + @Override + public void stop() { + try { + serverChannel.socket().close(); + selector.close(); + for (SelectionKey selectionKey : connections.values()) { + selectionKey.channel().close(); + } + } catch (IOException e) { + throw new ApplicationException(e); + } + } + + @Override + public void offer(InventoryVector iv) { + List<ConnectionInfo> target = new LinkedList<>(); + for (ConnectionInfo connection : connections.keySet()) { + if (connection.getState() == ACTIVE && !connection.knowsOf(iv)) { + target.add(connection); + } + } + List<ConnectionInfo> randomSubset = selectRandom(NETWORK_MAGIC_NUMBER, target); + for (ConnectionInfo connection : randomSubset) { + connection.offer(iv); + } + } + + @Override + public void request(Collection<InventoryVector> inventoryVectors) { + if (!isRunning()) { + requestedObjects.clear(); + return; + } + Iterator<InventoryVector> iterator = inventoryVectors.iterator(); + if (!iterator.hasNext()) { + return; + } + + Map<ConnectionInfo, List<InventoryVector>> distribution = new HashMap<>(); + for (ConnectionInfo connection : connections.keySet()) { + if (connection.getState() == ACTIVE) { + distribution.put(connection, new LinkedList<InventoryVector>()); + } + } + if (distribution.isEmpty()) { + return; + } + InventoryVector next = iterator.next(); + ConnectionInfo previous = null; + do { + for (ConnectionInfo connection : distribution.keySet()) { + if (connection == previous || previous == null) { + if (iterator.hasNext()) { + previous = connection; + next = iterator.next(); + } else { + break; + } + } + if (connection.knowsOf(next)) { + List<InventoryVector> ivs = distribution.get(connection); + if (ivs.size() == GetData.MAX_INVENTORY_SIZE) { + connection.send(new GetData.Builder().inventory(ivs).build()); + ivs.clear(); + } + ivs.add(next); + iterator.remove(); + + if (iterator.hasNext()) { + next = iterator.next(); + previous = connection; + } else { + break; + } + } + } + } while (iterator.hasNext()); + + // remove objects nobody knows of + requestedObjects.removeAll(inventoryVectors); + + for (ConnectionInfo connection : distribution.keySet()) { + List<InventoryVector> ivs = distribution.get(connection); + if (!ivs.isEmpty()) { + connection.send(new GetData.Builder().inventory(ivs).build()); + } + } + } + + @Override + public Property getNetworkStatus() { + TreeSet<Long> streams = new TreeSet<>(); + TreeMap<Long, Integer> incomingConnections = new TreeMap<>(); + TreeMap<Long, Integer> outgoingConnections = new TreeMap<>(); + + for (ConnectionInfo connection : connections.keySet()) { + if (connection.getState() == ACTIVE) { + for (long stream : connection.getStreams()) { + streams.add(stream); + if (connection.getMode() == SERVER) { + inc(incomingConnections, stream); + } else { + inc(outgoingConnections, stream); + } + } + } + } + Property[] streamProperties = new Property[streams.size()]; + int i = 0; + for (Long stream : streams) { + int incoming = incomingConnections.containsKey(stream) ? incomingConnections.get(stream) : 0; + int outgoing = outgoingConnections.containsKey(stream) ? outgoingConnections.get(stream) : 0; + streamProperties[i] = new Property("stream " + stream, + null, new Property("nodes", incoming + outgoing), + new Property("incoming", incoming), + new Property("outgoing", outgoing) + ); + i++; + } + return new Property("network", null, + new Property("connectionManager", isRunning() ? "running" : "stopped"), + new Property("connections", null, streamProperties), + new Property("requestedObjects", requestedObjects.size()) + ); + } + + private boolean isConnectedTo(NetworkAddress address) { + for (ConnectionInfo c : connections.keySet()) { + if (c.getNode().equals(address)) { + return true; + } + } + return false; + } + + @Override + public boolean isRunning() { + return selector != null && selector.isOpen() && starter.isAlive(); + } + + @Override + public void setContext(InternalContext context) { + this.ctx = context; + } +} 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 a45ec47..d64a855 100644 --- a/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkHandlerTest.java +++ b/networking/src/test/java/ch/dissem/bitmessage/networking/NetworkHandlerTest.java @@ -17,146 +17,250 @@ package ch.dissem.bitmessage.networking; import ch.dissem.bitmessage.BitmessageContext; -import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; -import ch.dissem.bitmessage.ports.AddressRepository; -import ch.dissem.bitmessage.ports.MessageRepository; -import ch.dissem.bitmessage.ports.NetworkHandler; -import ch.dissem.bitmessage.ports.ProofOfWorkRepository; import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; +import ch.dissem.bitmessage.entity.CustomMessage; +import ch.dissem.bitmessage.entity.MessagePayload; +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.utils.Property; -import org.junit.AfterClass; -import org.junit.BeforeClass; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; -import org.mockito.Mockito; +import org.junit.rules.DisableOnDebug; +import org.junit.rules.TestRule; +import org.junit.rules.Timeout; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.net.InetAddress; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.Future; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; /** - * FIXME: there really should be sensible tests for the network handler + * Tests network handlers. This test is parametrized, so it can test both the nio and classic implementation + * as well as their combinations. It might be slightly over the top and will most probably be cleaned up once + * the nio implementation is deemed stable. */ +@RunWith(Parameterized.class) public class NetworkHandlerTest { - private static NetworkAddress localhost = new NetworkAddress.Builder().ipv4(127, 0, 0, 1).port(6001).build(); + private static final Logger LOG = LoggerFactory.getLogger(NetworkHandlerTest.class); + private static NetworkAddress peerAddress = new NetworkAddress.Builder().ipv4(127, 0, 0, 1).port(6001).build(); - private static TestInventory peerInventory; - private static TestInventory nodeInventory; + private TestInventory peerInventory; + private TestInventory nodeInventory; - private static BitmessageContext peer; - private static BitmessageContext node; - private static NetworkHandler networkHandler; + private BitmessageContext peer; + private BitmessageContext node; - @BeforeClass - public static void setUp() { + private final NetworkHandler peerNetworkHandler; + private final NetworkHandler nodeNetworkHandler; + + @Rule + public final TestRule timeout = new DisableOnDebug(Timeout.seconds(60)); + + public NetworkHandlerTest(NetworkHandler peer, NetworkHandler node) { + this.peerNetworkHandler = peer; + this.nodeNetworkHandler = node; + } + + @Parameterized.Parameters + public static List<Object[]> parameters() { + return Arrays.asList(new Object[][]{ + {new DefaultNetworkHandler(), new DefaultNetworkHandler()}, + {new DefaultNetworkHandler(), new NioNetworkHandler()}, + {new NioNetworkHandler(), new DefaultNetworkHandler()}, + {new NioNetworkHandler(), new NioNetworkHandler()} + }); + } + + @Before + public void setUp() throws InterruptedException { peerInventory = new TestInventory(); peer = new BitmessageContext.Builder() - .addressRepo(Mockito.mock(AddressRepository.class)) - .inventory(peerInventory) - .messageRepo(Mockito.mock(MessageRepository.class)) - .powRepo(Mockito.mock(ProofOfWorkRepository.class)) - .port(6001) - .nodeRegistry(new TestNodeRegistry()) - .networkHandler(new DefaultNetworkHandler()) - .cryptography(new BouncyCryptography()) - .listener(Mockito.mock(BitmessageContext.Listener.class)) - .build(); + .addressRepo(mock(AddressRepository.class)) + .inventory(peerInventory) + .messageRepo(mock(MessageRepository.class)) + .powRepo(mock(ProofOfWorkRepository.class)) + .port(peerAddress.getPort()) + .nodeRegistry(new TestNodeRegistry()) + .networkHandler(peerNetworkHandler) + .cryptography(new BouncyCryptography()) + .listener(mock(BitmessageContext.Listener.class)) + .customCommandHandler(new CustomCommandHandler() { + @Override + public MessagePayload handle(CustomMessage request) { + byte[] data = request.getData(); + if (data.length > 0) { + switch (data[0]) { + case 0: + return null; + case 1: + break; + case 3: + data[0] = 0; + break; + default: + break; + } + } + return new CustomMessage("test response", request.getData()); + } + }) + .build(); peer.startup(); + Thread.sleep(100); nodeInventory = new TestInventory(); - networkHandler = new DefaultNetworkHandler(); node = new BitmessageContext.Builder() - .addressRepo(Mockito.mock(AddressRepository.class)) - .inventory(nodeInventory) - .messageRepo(Mockito.mock(MessageRepository.class)) - .powRepo(Mockito.mock(ProofOfWorkRepository.class)) - .port(6002) - .nodeRegistry(new TestNodeRegistry(localhost)) - .networkHandler(networkHandler) - .cryptography(new BouncyCryptography()) - .listener(Mockito.mock(BitmessageContext.Listener.class)) - .build(); + .addressRepo(mock(AddressRepository.class)) + .inventory(nodeInventory) + .messageRepo(mock(MessageRepository.class)) + .powRepo(mock(ProofOfWorkRepository.class)) + .port(6002) + .nodeRegistry(new TestNodeRegistry(peerAddress)) + .networkHandler(nodeNetworkHandler) + .cryptography(new BouncyCryptography()) + .listener(mock(BitmessageContext.Listener.class)) + .build(); } - @AfterClass - public static void cleanUp() { + @After + public void cleanUp() { shutdown(peer); + shutdown(node); + shutdown(nodeNetworkHandler); } - private static void shutdown(BitmessageContext node) { - node.shutdown(); + private static void shutdown(BitmessageContext ctx) { + if (!ctx.isRunning()) return; + + ctx.shutdown(); do { try { Thread.sleep(100); } catch (InterruptedException ignore) { } - } while (node.isRunning()); + } while (ctx.isRunning()); } - @Test(timeout = 5_000) - public void ensureNodesAreConnecting() { - try { - node.startup(); - Property status; - do { - Thread.yield(); - status = node.status().getProperty("network", "connections", "stream 0"); - } while (status == null); - assertEquals(1, status.getProperty("outgoing").getValue()); - } finally { - shutdown(node); - } + private static void shutdown(NetworkHandler networkHandler) { + if (!networkHandler.isRunning()) return; + + networkHandler.stop(); + do { + try { + Thread.sleep(100); + } catch (InterruptedException ignore) { + if (networkHandler.isRunning()) { + LOG.warn("Thread interrupted while waiting for network shutdown - " + + "this could cause problems in subsequent tests."); + } + return; + } + } while (networkHandler.isRunning()); } - @Test(timeout = 5_000) + private Property waitForNetworkStatus(BitmessageContext ctx) throws InterruptedException { + Property status; + do { + Thread.sleep(100); + status = ctx.status().getProperty("network", "connections", "stream 1"); + } while (status == null); + return status; + } + + @Test + public void ensureNodesAreConnecting() throws Exception { + node.startup(); + + Property nodeStatus = waitForNetworkStatus(node); + Property peerStatus = waitForNetworkStatus(peer); + + assertEquals(1, nodeStatus.getProperty("outgoing").getValue()); + assertEquals(1, peerStatus.getProperty("incoming").getValue()); + } + + @Test + public void ensureCustomMessageIsSentAndResponseRetrieved() throws Exception { + byte[] data = cryptography().randomBytes(8); + data[0] = (byte) 1; + CustomMessage request = new CustomMessage("test request", data); + node.startup(); + + CustomMessage response = nodeNetworkHandler.send(peerAddress.toInetAddress(), peerAddress.getPort(), request); + + assertThat(response, notNullValue()); + assertThat(response.getCustomCommand(), is("test response")); + assertThat(response.getData(), is(data)); + } + + @Test(expected = NodeException.class) + public void ensureCustomMessageWithoutResponseYieldsException() throws Exception { + byte[] data = cryptography().randomBytes(8); + data[0] = (byte) 0; + CustomMessage request = new CustomMessage("test request", data); + + CustomMessage response = nodeNetworkHandler.send(peerAddress.toInetAddress(), peerAddress.getPort(), request); + + assertThat(response, notNullValue()); + assertThat(response.getCustomCommand(), is("test response")); + assertThat(response.getData(), is(request.getData())); + } + + @Test public void ensureObjectsAreSynchronizedIfBothHaveObjects() throws Exception { peerInventory.init( - "V4Pubkey.payload", - "V5Broadcast.payload" + "V4Pubkey.payload", + "V5Broadcast.payload" ); nodeInventory.init( - "V1Msg.payload", - "V4Pubkey.payload" + "V1Msg.payload", + "V4Pubkey.payload" ); - Future<?> future = networkHandler.synchronize(InetAddress.getLocalHost(), 6001, - mock(NetworkHandler.MessageListener.class), - 10); + Future<?> future = nodeNetworkHandler.synchronize(peerAddress.toInetAddress(), peerAddress.getPort(), 10); future.get(); assertInventorySize(3, nodeInventory); assertInventorySize(3, peerInventory); } - @Test(timeout = 5_000) + @Test public void ensureObjectsAreSynchronizedIfOnlyPeerHasObjects() throws Exception { peerInventory.init( - "V4Pubkey.payload", - "V5Broadcast.payload" + "V4Pubkey.payload", + "V5Broadcast.payload" ); nodeInventory.init(); - Future<?> future = networkHandler.synchronize(InetAddress.getLocalHost(), 6001, - mock(NetworkHandler.MessageListener.class), - 10); + Future<?> future = nodeNetworkHandler.synchronize(peerAddress.toInetAddress(), peerAddress.getPort(), 10); future.get(); assertInventorySize(2, nodeInventory); assertInventorySize(2, peerInventory); } - @Test(timeout = 5_000) + @Test public void ensureObjectsAreSynchronizedIfOnlyNodeHasObjects() throws Exception { peerInventory.init(); nodeInventory.init( - "V1Msg.payload" + "V1Msg.payload" ); - Future<?> future = networkHandler.synchronize(InetAddress.getLocalHost(), 6001, - mock(NetworkHandler.MessageListener.class), - 10); + Future<?> future = nodeNetworkHandler.synchronize(peerAddress.toInetAddress(), peerAddress.getPort(), 10); future.get(); assertInventorySize(1, nodeInventory); assertInventorySize(1, peerInventory); @@ -169,4 +273,5 @@ public class NetworkHandlerTest { } assertEquals(expected, inventory.getInventory().size()); } + } diff --git a/repositories/build.gradle b/repositories/build.gradle index abb8651..3f5874b 100644 --- a/repositories/build.gradle +++ b/repositories/build.gradle @@ -14,9 +14,10 @@ sourceCompatibility = 1.8 dependencies { compile project(':core') - compile 'org.flywaydb:flyway-core:3.2.1' + compile 'org.flywaydb:flyway-core:4.0.3' testCompile 'junit:junit:4.12' - testCompile 'com.h2database:h2:1.4.190' + testCompile 'com.h2database:h2:1.4.192' testCompile 'org.mockito:mockito-core:1.10.19' + testCompile project(path: ':core', configuration: 'testArtifacts') testCompile project(':cryptography-bc') -} \ No newline at end of file +} diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcAddressRepository.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcAddressRepository.java index bf15a6a..5422997 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcAddressRepository.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcAddressRepository.java @@ -26,7 +26,6 @@ import ch.dissem.bitmessage.ports.AddressRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -71,6 +70,11 @@ public class JdbcAddressRepository extends JdbcHelper implements AddressReposito return find("private_key IS NOT NULL"); } + @Override + public List<BitmessageAddress> getChans() { + return find("chan = '1'"); + } + @Override public List<BitmessageAddress> getSubscriptions() { return find("subscribed = '1'"); @@ -87,22 +91,22 @@ public class JdbcAddressRepository extends JdbcHelper implements AddressReposito @Override public List<BitmessageAddress> getContacts() { - return find("private_key IS NULL"); + return find("private_key IS NULL OR chan = '1'"); } private List<BitmessageAddress> find(String where) { List<BitmessageAddress> result = new LinkedList<>(); - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT address, alias, public_key, private_key, subscribed FROM Address WHERE " + where); + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT address, alias, public_key, private_key, subscribed, chan " + + "FROM Address WHERE " + where) + ) { while (rs.next()) { BitmessageAddress address; InputStream privateKeyStream = rs.getBinaryStream("private_key"); - if (privateKeyStream != null) { - PrivateKey privateKey = PrivateKey.read(privateKeyStream); - address = new BitmessageAddress(privateKey); - } else { + if (privateKeyStream == null) { address = new BitmessageAddress(rs.getString("address")); Blob publicKeyBlob = rs.getBlob("public_key"); if (publicKeyBlob != null) { @@ -113,9 +117,13 @@ public class JdbcAddressRepository extends JdbcHelper implements AddressReposito } address.setPubkey(pubkey); } + } else { + PrivateKey privateKey = PrivateKey.read(privateKeyStream); + address = new BitmessageAddress(privateKey); } address.setAlias(rs.getString("alias")); address.setSubscribed(rs.getBoolean("subscribed")); + address.setChan(rs.getBoolean("chan")); result.add(address); } @@ -126,11 +134,15 @@ public class JdbcAddressRepository extends JdbcHelper implements AddressReposito } private boolean exists(BitmessageAddress address) { - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM Address WHERE address='" + address.getAddress() + "'"); - rs.next(); - return rs.getInt(1) > 0; + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM Address " + + "WHERE address='" + address.getAddress() + "'") + ) { + if (rs.next()) { + return rs.getInt(1) > 0; + } } catch (SQLException e) { LOG.error(e.getMessage(), e); } @@ -151,16 +163,18 @@ public class JdbcAddressRepository extends JdbcHelper implements AddressReposito } private void update(BitmessageAddress address) throws IOException, SQLException { - try (Connection connection = config.getConnection()) { - StringBuilder statement = new StringBuilder("UPDATE Address SET alias=?"); - if (address.getPubkey() != null) { - statement.append(", public_key=?"); - } - if (address.getPrivateKey() != null) { - statement.append(", private_key=?"); - } - statement.append(", subscribed=? WHERE address=?"); - PreparedStatement ps = connection.prepareStatement(statement.toString()); + StringBuilder statement = new StringBuilder("UPDATE Address SET alias=?"); + if (address.getPubkey() != null) { + statement.append(", public_key=?"); + } + if (address.getPrivateKey() != null) { + statement.append(", private_key=?"); + } + statement.append(", subscribed=?, chan=? WHERE address=?"); + try ( + Connection connection = config.getConnection(); + PreparedStatement ps = connection.prepareStatement(statement.toString()) + ) { int i = 0; ps.setString(++i, address.getAlias()); if (address.getPubkey() != null) { @@ -170,21 +184,26 @@ public class JdbcAddressRepository extends JdbcHelper implements AddressReposito writeBlob(ps, ++i, address.getPrivateKey()); } ps.setBoolean(++i, address.isSubscribed()); + ps.setBoolean(++i, address.isChan()); ps.setString(++i, address.getAddress()); ps.executeUpdate(); } } private void insert(BitmessageAddress address) throws IOException, SQLException { - try (Connection connection = config.getConnection()) { - PreparedStatement ps = connection.prepareStatement( - "INSERT INTO Address (address, version, alias, public_key, private_key, subscribed) VALUES (?, ?, ?, ?, ?, ?)"); + try ( + Connection connection = config.getConnection(); + PreparedStatement ps = connection.prepareStatement( + "INSERT INTO Address (address, version, alias, public_key, private_key, subscribed, chan) " + + "VALUES (?, ?, ?, ?, ?, ?, ?)") + ) { ps.setString(1, address.getAddress()); ps.setLong(2, address.getVersion()); ps.setString(3, address.getAlias()); writePubkey(ps, 4, address.getPubkey()); writeBlob(ps, 5, address.getPrivateKey()); ps.setBoolean(6, address.isSubscribed()); + ps.setBoolean(7, address.isChan()); ps.executeUpdate(); } } @@ -201,8 +220,10 @@ public class JdbcAddressRepository extends JdbcHelper implements AddressReposito @Override public void remove(BitmessageAddress address) { - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement() + ) { stmt.executeUpdate("DELETE FROM Address WHERE address = '" + address.getAddress() + "'"); } catch (SQLException e) { LOG.error(e.getMessage(), e); diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcHelper.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcHelper.java index d583a71..b0be9c1 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcHelper.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcHelper.java @@ -18,6 +18,7 @@ package ch.dissem.bitmessage.repository; import ch.dissem.bitmessage.entity.Streamable; import ch.dissem.bitmessage.entity.payload.ObjectType; +import ch.dissem.bitmessage.exception.ApplicationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,6 +26,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.util.Collection; import static ch.dissem.bitmessage.utils.Strings.hex; @@ -40,49 +42,13 @@ public abstract class JdbcHelper { this.config = config; } - public static StringBuilder join(long... objects) { - StringBuilder streamList = new StringBuilder(); - for (int i = 0; i < objects.length; i++) { - if (i > 0) streamList.append(", "); - streamList.append(objects[i]); - } - return streamList; - } - - public static StringBuilder join(byte[]... objects) { - StringBuilder streamList = new StringBuilder(); - for (int i = 0; i < objects.length; i++) { - if (i > 0) streamList.append(", "); - streamList.append(hex(objects[i])); - } - return streamList; - } - - public static StringBuilder join(ObjectType... types) { - StringBuilder streamList = new StringBuilder(); - for (int i = 0; i < types.length; i++) { - if (i > 0) streamList.append(", "); - streamList.append(types[i].getNumber()); - } - return streamList; - } - - public static StringBuilder join(Enum... types) { - StringBuilder streamList = new StringBuilder(); - for (int i = 0; i < types.length; i++) { - if (i > 0) streamList.append(", "); - streamList.append('\'').append(types[i].name()).append('\''); - } - return streamList; - } - - protected void writeBlob(PreparedStatement ps, int parameterIndex, Streamable data) throws SQLException, IOException { - if (data != null) { + public static void writeBlob(PreparedStatement ps, int parameterIndex, Streamable data) throws SQLException, IOException { + if (data == null) { + ps.setBytes(parameterIndex, null); + } else { ByteArrayOutputStream os = new ByteArrayOutputStream(); data.write(os); ps.setBytes(parameterIndex, os.toByteArray()); - } else { - ps.setBytes(parameterIndex, null); } } } diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcInventory.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcInventory.java index 3336475..13d3725 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcInventory.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcInventory.java @@ -19,6 +19,7 @@ package ch.dissem.bitmessage.repository; import ch.dissem.bitmessage.entity.ObjectMessage; import ch.dissem.bitmessage.entity.payload.ObjectType; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.ports.Inventory; import org.slf4j.Logger; @@ -30,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import static ch.dissem.bitmessage.utils.SqlStrings.join; import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; import static ch.dissem.bitmessage.utils.UnixTime.now; @@ -60,11 +62,12 @@ public class JdbcInventory extends JdbcHelper implements Inventory { if (cache.get(stream) == null) { result = new ConcurrentHashMap<>(); cache.put(stream, result); - - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT hash, expires FROM Inventory WHERE expires > " - + now(-5 * MINUTE) + " AND stream = " + stream); + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT hash, expires FROM Inventory " + + "WHERE expires > " + now(-5 * MINUTE) + " AND stream = " + stream) + ) { while (rs.next()) { result.put(new InventoryVector(rs.getBytes("hash")), rs.getLong("expires")); } @@ -87,9 +90,11 @@ public class JdbcInventory extends JdbcHelper implements Inventory { @Override public ObjectMessage getObject(InventoryVector vector) { - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT data, version FROM Inventory WHERE hash = X'" + vector + "'"); + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT data, version FROM Inventory WHERE hash = X'" + vector + "'") + ) { if (rs.next()) { Blob data = rs.getBlob("data"); return Factory.getObjectMessage(rs.getInt("version"), data.getBinaryStream(), (int) data.length()); @@ -99,25 +104,27 @@ public class JdbcInventory extends JdbcHelper implements Inventory { } } catch (Exception e) { LOG.error(e.getMessage(), e); - throw new RuntimeException(e); + throw new ApplicationException(e); } } @Override public List<ObjectMessage> getObjects(long stream, long version, ObjectType... types) { - try (Connection connection = config.getConnection()) { - StringBuilder query = new StringBuilder("SELECT data, version FROM Inventory WHERE 1=1"); - if (stream > 0) { - query.append(" AND stream = ").append(stream); - } - if (version > 0) { - query.append(" AND version = ").append(version); - } - if (types.length > 0) { - query.append(" AND type IN (").append(join(types)).append(")"); - } - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery(query.toString()); + StringBuilder query = new StringBuilder("SELECT data, version FROM Inventory WHERE 1=1"); + if (stream > 0) { + query.append(" AND stream = ").append(stream); + } + if (version > 0) { + query.append(" AND version = ").append(version); + } + if (types.length > 0) { + query.append(" AND type IN (").append(join(types)).append(')'); + } + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(query.toString()) + ) { List<ObjectMessage> result = new LinkedList<>(); while (rs.next()) { Blob data = rs.getBlob("data"); @@ -126,7 +133,7 @@ public class JdbcInventory extends JdbcHelper implements Inventory { return result; } catch (Exception e) { LOG.error(e.getMessage(), e); - throw new RuntimeException(e); + throw new ApplicationException(e); } } @@ -135,8 +142,11 @@ public class JdbcInventory extends JdbcHelper implements Inventory { if (getCache(object.getStream()).containsKey(object.getInventoryVector())) return; - try (Connection connection = config.getConnection()) { - PreparedStatement ps = connection.prepareStatement("INSERT INTO Inventory (hash, stream, expires, data, type, version) VALUES (?, ?, ?, ?, ?, ?)"); + try ( + Connection connection = config.getConnection(); + PreparedStatement ps = connection.prepareStatement("INSERT INTO Inventory " + + "(hash, stream, expires, data, type, version) VALUES (?, ?, ?, ?, ?, ?)") + ) { InventoryVector iv = object.getInventoryVector(); LOG.trace("Storing object " + iv); ps.setBytes(1, iv.getHash()); @@ -162,8 +172,11 @@ public class JdbcInventory extends JdbcHelper implements Inventory { @Override public void cleanup() { - try (Connection connection = config.getConnection()) { - connection.createStatement().executeUpdate("DELETE FROM Inventory WHERE expires < " + now(-5 * MINUTE)); + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement() + ) { + stmt.executeUpdate("DELETE FROM Inventory WHERE expires < " + now(-5 * MINUTE)); } catch (SQLException e) { LOG.debug(e.getMessage(), e); } 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 69890c3..3788d96 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcMessageRepository.java @@ -16,13 +16,12 @@ package ch.dissem.bitmessage.repository; -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.ports.AbstractMessageRepository; import ch.dissem.bitmessage.ports.MessageRepository; -import ch.dissem.bitmessage.utils.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,32 +29,30 @@ import java.io.IOException; import java.io.InputStream; import java.sql.*; import java.util.ArrayList; -import java.util.Collection; import java.util.LinkedList; import java.util.List; -public class JdbcMessageRepository extends JdbcHelper implements MessageRepository, InternalContext.ContextHolder { +import static ch.dissem.bitmessage.repository.JdbcHelper.writeBlob; + +public class JdbcMessageRepository extends AbstractMessageRepository implements MessageRepository { private static final Logger LOG = LoggerFactory.getLogger(JdbcMessageRepository.class); - private InternalContext ctx; + private final JdbcConfig config; public JdbcMessageRepository(JdbcConfig config) { - super(config); + this.config = config; } @Override - public List<Label> getLabels() { - List<Label> result = new LinkedList<>(); - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT id, label, type, color FROM Label ORDER BY ord"); - while (rs.next()) { - result.add(getLabel(rs)); - } + protected List<Label> findLabels(String where) { + try ( + Connection connection = config.getConnection() + ) { + return findLabels(connection, where); } catch (SQLException e) { - throw new RuntimeException(e); + LOG.error(e.getMessage(), e); } - return result; + return new ArrayList<>(); } private Label getLabel(ResultSet rs) throws SQLException { @@ -70,36 +67,22 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito return label; } - @Override - public List<Label> getLabels(Label.Type... types) { - List<Label> result = new LinkedList<>(); - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT id, label, type, color FROM Label WHERE type IN (" + join(types) + - ") ORDER BY ord"); - while (rs.next()) { - result.add(getLabel(rs)); - } - } catch (SQLException e) { - LOG.error(e.getMessage(), e); - } - return result; - } - @Override public int countUnread(Label label) { String where; - if (label != null) { - where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ") AND "; - } else { + if (label == null) { where = ""; + } else { + 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() + "'))"; - try (Connection connection = config.getConnection()) { - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT count(*) FROM Message WHERE " + where); + try ( + 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); } @@ -110,44 +93,15 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito } @Override - public Plaintext getMessage(byte[] initialHash) { - List<Plaintext> plaintexts = find("initial_hash=X'" + Strings.hex(initialHash) + "'"); - switch (plaintexts.size()) { - case 0: - return null; - case 1: - return plaintexts.get(0); - default: - throw new RuntimeException("This shouldn't happen, found " + plaintexts.size() + - " messages, one or none was expected"); - } - } - - @Override - public List<Plaintext> findMessages(Label label) { - return find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")"); - } - - @Override - public List<Plaintext> findMessages(Plaintext.Status status, BitmessageAddress recipient) { - return find("status='" + status.name() + "' AND recipient='" + recipient.getAddress() + "'"); - } - - @Override - public List<Plaintext> findMessages(Plaintext.Status status) { - return find("status='" + status.name() + "'"); - } - - @Override - public List<Plaintext> findMessages(BitmessageAddress sender) { - return find("sender='" + sender.getAddress() + "'"); - } - - private List<Plaintext> find(String where) { + 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, sent, received, status FROM Message WHERE " + where); + 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) + ) { while (rs.next()) { byte[] iv = rs.getBytes("iv"); InputStream data = rs.getBinaryStream("data"); @@ -158,11 +112,18 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito builder.IV(new InventoryVector(iv)); 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.status(Plaintext.Status.valueOf(rs.getString("status"))); - builder.labels(findLabels(connection, id)); - result.add(builder.build()); + builder.ttl(rs.getLong("ttl")); + 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")); + Plaintext message = builder.build(); + message.setInitialHash(rs.getBytes("initial_hash")); + result.add(message); } } catch (IOException | SQLException e) { LOG.error(e.getMessage(), e); @@ -170,11 +131,12 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito return result; } - private Collection<Label> findLabels(Connection connection, long messageId) { + 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 id IN (SELECT label_id FROM Message_Label WHERE message_id=" + messageId + ")"); + try ( + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT id, label, type, color FROM Label WHERE " + where) + ) { while (rs.next()) { result.add(getLabel(rs)); } @@ -186,92 +148,104 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito @Override public void save(Plaintext message) { - // save from address if necessary - if (message.getId() == null) { - BitmessageAddress savedAddress = ctx.getAddressRepository().getAddress(message.getFrom().getAddress()); - if (savedAddress == null || savedAddress.getPrivateKey() == null) { - if (savedAddress != null && savedAddress.getAlias() != null) { - message.getFrom().setAlias(savedAddress.getAlias()); - } - ctx.getAddressRepository().save(message.getFrom()); - } - } + safeSenderIfNecessary(message); try (Connection connection = config.getConnection()) { try { connection.setAutoCommit(false); - // save message - if (message.getId() == null) { - insert(connection, message); - } else { - update(connection, message); - } - - // remove existing labels - Statement stmt = connection.createStatement(); - stmt.executeUpdate("DELETE FROM Message_Label WHERE message_id=" + message.getId()); - - // save labels - PreparedStatement ps = connection.prepareStatement("INSERT INTO Message_Label VALUES (" + message.getId() + ", ?)"); - for (Label label : message.getLabels()) { - ps.setLong(1, (Long) label.getId()); - ps.executeUpdate(); - } - + save(connection, message); + updateLabels(connection, message); connection.commit(); } catch (IOException | SQLException e) { - try { - connection.rollback(); - } catch (SQLException e1) { - LOG.debug(e1.getMessage(), e); - } - throw new RuntimeException(e); + connection.rollback(); + throw e; + } + } catch (IOException | SQLException e) { + throw new ApplicationException(e); + } + } + + private void save(Connection connection, Plaintext message) throws IOException, SQLException { + if (message.getId() == null) { + insert(connection, message); + } else { + update(connection, message); + } + } + + private void updateLabels(Connection connection, Plaintext message) throws SQLException { + // remove existing labels + try (Statement stmt = connection.createStatement()) { + stmt.executeUpdate("DELETE FROM Message_Label WHERE message_id=" + message.getId()); + } + // save new labels + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO Message_Label VALUES (" + + message.getId() + ", ?)")) { + for (Label label : message.getLabels()) { + ps.setLong(1, (Long) label.getId()); + ps.executeUpdate(); } - } catch (SQLException e) { - throw new RuntimeException(e); } } private void insert(Connection connection, Plaintext message) throws SQLException, IOException { - PreparedStatement ps = connection.prepareStatement( - "INSERT INTO Message (iv, type, sender, recipient, data, sent, received, status, initial_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", - Statement.RETURN_GENERATED_KEYS); - ps.setBytes(1, message.getInventoryVector() != null ? message.getInventoryVector().getHash() : null); - ps.setString(2, message.getType().name()); - ps.setString(3, message.getFrom().getAddress()); - ps.setString(4, message.getTo() != null ? message.getTo().getAddress() : null); - writeBlob(ps, 5, message); - ps.setLong(6, message.getSent()); - ps.setLong(7, message.getReceived()); - ps.setString(8, message.getStatus() != null ? message.getStatus().name() : null); - ps.setBytes(9, message.getInitialHash()); + 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) + ) { + ps.setBytes(1, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash()); + ps.setString(2, message.getType().name()); + ps.setString(3, message.getFrom().getAddress()); + 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.setString(9, message.getStatus() == null ? null : message.getStatus().name()); + ps.setBytes(10, message.getInitialHash()); + ps.setLong(11, message.getTTL()); + ps.setInt(12, message.getRetries()); + ps.setObject(13, message.getNextTry()); - ps.executeUpdate(); - - // get generated id - ResultSet rs = ps.getGeneratedKeys(); - rs.next(); - message.setId(rs.getLong(1)); + ps.executeUpdate(); + // get generated id + try (ResultSet rs = ps.getGeneratedKeys()) { + rs.next(); + message.setId(rs.getLong(1)); + } + } } private void update(Connection connection, Plaintext message) throws SQLException, IOException { - PreparedStatement ps = connection.prepareStatement( - "UPDATE Message SET iv=?, sent=?, received=?, status=?, initial_hash=? WHERE id=?"); - ps.setBytes(1, message.getInventoryVector() != null ? message.getInventoryVector().getHash() : null); - ps.setLong(2, message.getSent()); - ps.setLong(3, message.getReceived()); - ps.setString(4, message.getStatus() != null ? message.getStatus().name() : null); - ps.setBytes(5, message.getInitialHash()); - ps.setLong(6, (Long) message.getId()); - ps.executeUpdate(); + 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=?")) { + ps.setBytes(1, message.getInventoryVector() == null ? null : message.getInventoryVector().getHash()); + ps.setString(2, message.getType().name()); + ps.setString(3, message.getFrom().getAddress()); + 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.setString(9, message.getStatus() == null ? null : message.getStatus().name()); + ps.setBytes(10, message.getInitialHash()); + ps.setLong(11, message.getTTL()); + ps.setInt(12, message.getRetries()); + ps.setObject(13, message.getNextTry()); + ps.setLong(14, (Long) message.getId()); + ps.executeUpdate(); + } } @Override public void remove(Plaintext message) { try (Connection connection = config.getConnection()) { - try { - connection.setAutoCommit(false); - Statement stmt = connection.createStatement(); + connection.setAutoCommit(false); + try (Statement stmt = connection.createStatement()) { stmt.executeUpdate("DELETE FROM Message_Label WHERE message_id = " + message.getId()); stmt.executeUpdate("DELETE FROM Message WHERE id = " + message.getId()); connection.commit(); @@ -287,9 +261,4 @@ public class JdbcMessageRepository extends JdbcHelper implements MessageReposito LOG.error(e.getMessage(), e); } } - - @Override - public void setContext(InternalContext context) { - this.ctx = context; - } } diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcNodeRegistry.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcNodeRegistry.java new file mode 100644 index 0000000..07d343a --- /dev/null +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcNodeRegistry.java @@ -0,0 +1,167 @@ +package ch.dissem.bitmessage.repository; + +import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; +import ch.dissem.bitmessage.exception.ApplicationException; +import ch.dissem.bitmessage.ports.NodeRegistry; +import ch.dissem.bitmessage.utils.Collections; +import ch.dissem.bitmessage.utils.SqlStrings; +import ch.dissem.bitmessage.utils.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.*; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static ch.dissem.bitmessage.ports.NodeRegistryHelper.loadStableNodes; +import static ch.dissem.bitmessage.utils.UnixTime.*; + +public class JdbcNodeRegistry extends JdbcHelper implements NodeRegistry { + private static final Logger LOG = LoggerFactory.getLogger(JdbcNodeRegistry.class); + private Map<Long, Set<NetworkAddress>> stableNodes; + + public JdbcNodeRegistry(JdbcConfig config) { + super(config); + cleanUp(); + } + + private void cleanUp() { + try ( + Connection connection = config.getConnection(); + PreparedStatement ps = connection.prepareStatement( + "DELETE FROM Node WHERE time<?") + ) { + ps.setLong(1, now(-28 * DAY)); + ps.executeUpdate(); + } catch (SQLException e) { + LOG.error(e.getMessage(), e); + } + } + + private NetworkAddress loadExisting(NetworkAddress node) { + String query = + "SELECT stream, address, port, services, time" + + " FROM Node" + + " WHERE stream = " + node.getStream() + + " AND address = X'" + Strings.hex(node.getIPv6()) + "'" + + " AND port = " + node.getPort(); + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(query) + ) { + if (rs.next()) { + return new NetworkAddress.Builder() + .stream(rs.getLong("stream")) + .ipv6(rs.getBytes("address")) + .port(rs.getInt("port")) + .services(rs.getLong("services")) + .time(rs.getLong("time")) + .build(); + } else { + return null; + } + } catch (Exception e) { + LOG.error(e.getMessage(), e); + throw new ApplicationException(e); + } + } + + @Override + public List<NetworkAddress> getKnownAddresses(int limit, long... streams) { + List<NetworkAddress> result = new LinkedList<>(); + String query = + "SELECT stream, address, port, services, time" + + " FROM Node WHERE stream IN (" + SqlStrings.join(streams) + ")" + + " ORDER BY TIME DESC" + + " LIMIT " + limit; + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(query) + ) { + while (rs.next()) { + result.add( + new NetworkAddress.Builder() + .stream(rs.getLong("stream")) + .ipv6(rs.getBytes("address")) + .port(rs.getInt("port")) + .services(rs.getLong("services")) + .time(rs.getLong("time")) + .build() + ); + } + } catch (Exception e) { + LOG.error(e.getMessage(), e); + throw new ApplicationException(e); + } + if (result.isEmpty()) { + synchronized (this) { + if (stableNodes == null) { + stableNodes = loadStableNodes(); + } + } + for (long stream : streams) { + Set<NetworkAddress> nodes = stableNodes.get(stream); + if (nodes != null && !nodes.isEmpty()) { + result.add(Collections.selectRandom(nodes)); + } + } + } + return result; + } + + @Override + public void offerAddresses(List<NetworkAddress> nodes) { + cleanUp(); + nodes.stream() + .filter(node -> node.getTime() < now(+2 * MINUTE) && node.getTime() > now(-28 * DAY)) + .forEach(node -> { + synchronized (this) { + NetworkAddress existing = loadExisting(node); + if (existing == null) { + insert(node); + } else if (node.getTime() > existing.getTime()) { + update(node); + } + } + }); + } + + private void insert(NetworkAddress node) { + try ( + Connection connection = config.getConnection(); + PreparedStatement ps = connection.prepareStatement( + "INSERT INTO Node (stream, address, port, services, time) " + + "VALUES (?, ?, ?, ?, ?)") + ) { + ps.setLong(1, node.getStream()); + ps.setBytes(2, node.getIPv6()); + ps.setInt(3, node.getPort()); + ps.setLong(4, node.getServices()); + ps.setLong(5, node.getTime()); + ps.executeUpdate(); + } catch (SQLException e) { + LOG.error(e.getMessage(), e); + } + } + + private void update(NetworkAddress node) { + try ( + Connection connection = config.getConnection(); + PreparedStatement ps = connection.prepareStatement( + "UPDATE Node SET services=?, time=? WHERE stream=? AND address=? AND port=?") + ) { + ps.setLong(1, node.getServices()); + ps.setLong(2, node.getTime()); + ps.setLong(3, node.getStream()); + ps.setBytes(4, node.getIPv6()); + ps.setInt(5, node.getPort()); + ps.executeUpdate(); + } catch (SQLException e) { + LOG.error(e.getMessage(), e); + } + } +} diff --git a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepository.java b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepository.java index 9268311..0fda3fa 100644 --- a/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepository.java +++ b/repositories/src/main/java/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepository.java @@ -1,23 +1,27 @@ package ch.dissem.bitmessage.repository; +import ch.dissem.bitmessage.InternalContext; import ch.dissem.bitmessage.entity.ObjectMessage; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.ports.ProofOfWorkRepository; import ch.dissem.bitmessage.utils.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.sql.*; import java.util.LinkedList; import java.util.List; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; /** * @author Christian Basler */ -public class JdbcProofOfWorkRepository extends JdbcHelper implements ProofOfWorkRepository { +public class JdbcProofOfWorkRepository extends JdbcHelper implements ProofOfWorkRepository, InternalContext.ContextHolder { private static final Logger LOG = LoggerFactory.getLogger(JdbcProofOfWorkRepository.class); + private InternalContext ctx; public JdbcProofOfWorkRepository(JdbcConfig config) { super(config); @@ -25,69 +29,106 @@ public class JdbcProofOfWorkRepository extends JdbcHelper implements ProofOfWork @Override public Item getItem(byte[] initialHash) { - try (Connection connection = config.getConnection()) { - PreparedStatement ps = connection.prepareStatement("SELECT data, version, nonce_trials_per_byte, extra_bytes FROM POW WHERE initial_hash=?"); + try ( + Connection connection = config.getConnection(); + PreparedStatement ps = connection.prepareStatement("SELECT data, version, nonce_trials_per_byte, " + + "extra_bytes, expiration_time, message_id FROM POW WHERE initial_hash=?") + ) { ps.setBytes(1, initialHash); - ResultSet rs = ps.executeQuery(); - if (rs.next()) { - Blob data = rs.getBlob("data"); - return new Item( - Factory.getObjectMessage(rs.getInt("version"), data.getBinaryStream(), (int) data.length()), - rs.getLong("nonce_trials_per_byte"), - rs.getLong("extra_bytes") - ); - } else { - throw new RuntimeException("Object requested that we don't have. Initial hash: " + Strings.hex(initialHash)); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + Blob data = rs.getBlob("data"); + if (rs.getObject("message_id") == null) { + return new Item( + Factory.getObjectMessage(rs.getInt("version"), data.getBinaryStream(), (int) data.length()), + rs.getLong("nonce_trials_per_byte"), + rs.getLong("extra_bytes") + ); + } else { + return new Item( + Factory.getObjectMessage(rs.getInt("version"), data.getBinaryStream(), (int) data.length()), + rs.getLong("nonce_trials_per_byte"), + rs.getLong("extra_bytes"), + rs.getLong("expiration_time"), + ctx.getMessageRepository().getMessage(rs.getLong("message_id")) + ); + } + } else { + throw new IllegalArgumentException("Object requested that we don't have. Initial hash: " + Strings.hex(initialHash)); + } } - } catch (Exception e) { + } catch (SQLException e) { LOG.error(e.getMessage(), e); - throw new RuntimeException(e); + throw new ApplicationException(e); } } @Override public List<byte[]> getItems() { - try (Connection connection = config.getConnection()) { + try ( + Connection connection = config.getConnection(); + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT initial_hash FROM POW") + ) { List<byte[]> result = new LinkedList<>(); - Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT initial_hash FROM POW"); while (rs.next()) { result.add(rs.getBytes("initial_hash")); } return result; } catch (SQLException e) { LOG.error(e.getMessage(), e); - throw new RuntimeException(e); + throw new ApplicationException(e); + } + } + + @Override + public void putObject(Item item) { + try ( + Connection connection = config.getConnection(); + PreparedStatement ps = connection.prepareStatement("INSERT INTO POW (initial_hash, data, version, " + + "nonce_trials_per_byte, extra_bytes, expiration_time, message_id) " + + "VALUES (?, ?, ?, ?, ?, ?, ?)") + ) { + ps.setBytes(1, cryptography().getInitialHash(item.object)); + writeBlob(ps, 2, item.object); + ps.setLong(3, item.object.getVersion()); + ps.setLong(4, item.nonceTrialsPerByte); + ps.setLong(5, item.extraBytes); + + if (item.message == null) { + ps.setObject(6, null); + ps.setObject(7, null); + } else { + ps.setLong(6, item.expirationTime); + ps.setLong(7, (Long) item.message.getId()); + } + ps.executeUpdate(); + } catch (IOException | SQLException e) { + LOG.debug("Error storing object of type " + item.object.getPayload().getClass().getSimpleName(), e); + throw new ApplicationException(e); } } @Override public void putObject(ObjectMessage object, long nonceTrialsPerByte, long extraBytes) { - try (Connection connection = config.getConnection()) { - PreparedStatement ps = connection.prepareStatement("INSERT INTO POW (initial_hash, data, version, nonce_trials_per_byte, extra_bytes) VALUES (?, ?, ?, ?, ?)"); - ps.setBytes(1, security().getInitialHash(object)); - writeBlob(ps, 2, object); - ps.setLong(3, object.getVersion()); - ps.setLong(4, nonceTrialsPerByte); - ps.setLong(5, extraBytes); - ps.executeUpdate(); - } catch (SQLException e) { - LOG.debug("Error storing object of type " + object.getPayload().getClass().getSimpleName(), e); - throw new RuntimeException(e); - } catch (Exception e) { - LOG.error(e.getMessage(), e); - throw new RuntimeException(e); - } + putObject(new Item(object, nonceTrialsPerByte, extraBytes)); } @Override public void removeObject(byte[] initialHash) { - try (Connection connection = config.getConnection()) { - PreparedStatement ps = connection.prepareStatement("DELETE FROM POW WHERE initial_hash=?"); + try ( + Connection connection = config.getConnection(); + PreparedStatement ps = connection.prepareStatement("DELETE FROM POW WHERE initial_hash=?") + ) { ps.setBytes(1, initialHash); ps.executeUpdate(); } catch (SQLException e) { LOG.debug(e.getMessage(), e); } } + + @Override + public void setContext(InternalContext context) { + this.ctx = context; + } } diff --git a/repositories/src/main/resources/db/migration/V3.0__Update_table_address.sql b/repositories/src/main/resources/db/migration/V3.0__Update_table_address.sql new file mode 100644 index 0000000..01e036b --- /dev/null +++ b/repositories/src/main/resources/db/migration/V3.0__Update_table_address.sql @@ -0,0 +1 @@ +ALTER TABLE Address ADD COLUMN chan BIT NOT NULL DEFAULT '0'; diff --git a/repositories/src/main/resources/db/migration/V3.1__Update_table_POW.sql b/repositories/src/main/resources/db/migration/V3.1__Update_table_POW.sql new file mode 100644 index 0000000..d67a1b5 --- /dev/null +++ b/repositories/src/main/resources/db/migration/V3.1__Update_table_POW.sql @@ -0,0 +1,2 @@ +ALTER TABLE POW ADD COLUMN expiration_time BIGINT; +ALTER TABLE POW ADD COLUMN message_id BIGINT; diff --git a/repositories/src/main/resources/db/migration/V3.2__Update_table_message.sql b/repositories/src/main/resources/db/migration/V3.2__Update_table_message.sql new file mode 100644 index 0000000..1eba39f --- /dev/null +++ b/repositories/src/main/resources/db/migration/V3.2__Update_table_message.sql @@ -0,0 +1,4 @@ +ALTER TABLE Message ADD COLUMN ack_data BINARY(32); +ALTER TABLE Message ADD COLUMN ttl BIGINT NOT NULL DEFAULT 0; +ALTER TABLE Message ADD COLUMN retries INT NOT NULL DEFAULT 0; +ALTER TABLE Message ADD COLUMN next_try BIGINT; diff --git a/repositories/src/main/resources/db/migration/V3.3__Create_table_node.sql b/repositories/src/main/resources/db/migration/V3.3__Create_table_node.sql new file mode 100644 index 0000000..5d03bb5 --- /dev/null +++ b/repositories/src/main/resources/db/migration/V3.3__Create_table_node.sql @@ -0,0 +1,9 @@ +CREATE TABLE Node ( + stream BIGINT NOT NULL, + address BINARY(32) NOT NULL, + port INT NOT NULL, + services BIGINT NOT NULL, + time BIGINT NOT NULL, + PRIMARY KEY (stream, address, port) +); +CREATE INDEX idx_time on Node(time); diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcAddressRepositoryTest.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcAddressRepositoryTest.java index 18e70b2..7f3098b 100644 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcAddressRepositoryTest.java +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcAddressRepositoryTest.java @@ -17,12 +17,14 @@ package ch.dissem.bitmessage.repository; import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.payload.Pubkey; import ch.dissem.bitmessage.entity.valueobject.PrivateKey; import org.junit.Before; import org.junit.Test; import java.util.List; +import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK; import static org.junit.Assert.*; public class JdbcAddressRepositoryTest extends TestBase { @@ -46,7 +48,7 @@ public class JdbcAddressRepositoryTest extends TestBase { repo.save(new BitmessageAddress(CONTACT_B)); repo.save(new BitmessageAddress(CONTACT_C)); - BitmessageAddress identityA = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000)); + BitmessageAddress identityA = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000, DOES_ACK)); repo.save(identityA); IDENTITY_A = identityA.getAddress(); BitmessageAddress identityB = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000)); @@ -66,8 +68,11 @@ public class JdbcAddressRepositoryTest extends TestBase { public void testFindIdentity() throws Exception { BitmessageAddress identity = new BitmessageAddress(IDENTITY_A); assertEquals(4, identity.getVersion()); - assertEquals(identity, repo.findIdentity(identity.getTag())); assertNull(repo.findContact(identity.getTag())); + + BitmessageAddress storedIdentity = repo.findIdentity(identity.getTag()); + assertEquals(identity, storedIdentity); + assertTrue(storedIdentity.has(Pubkey.Feature.DOES_ACK)); } @Test @@ -95,7 +100,7 @@ public class JdbcAddressRepositoryTest extends TestBase { addSubscription("BM-2D9QKN4teYRvoq2fyzpiftPh9WP9qggtzh"); List<BitmessageAddress> subscriptions; - + subscriptions = repo.getSubscriptions(5); assertEquals(1, subscriptions.size()); @@ -137,6 +142,24 @@ public class JdbcAddressRepositoryTest extends TestBase { assertNotNull(identityA.getPubkey()); assertNotNull(identityA.getPrivateKey()); assertEquals("Test", identityA.getAlias()); + assertFalse(identityA.isChan()); + } + + @Test + public void ensureNewChanIsSavedAndUpdated() { + BitmessageAddress chan = BitmessageAddress.chan(1, "test"); + repo.save(chan); + BitmessageAddress address = repo.getAddress(chan.getAddress()); + assertNotNull(address); + assertTrue(address.isChan()); + + address.setAlias("Test"); + repo.save(address); + + address = repo.getAddress(chan.getAddress()); + assertNotNull(address); + assertTrue(address.isChan()); + assertEquals("Test", address.getAlias()); } @Test 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 c7c3614..9d9b002 100644 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcMessageRepositoryTest.java @@ -19,12 +19,14 @@ package ch.dissem.bitmessage.repository; import ch.dissem.bitmessage.BitmessageContext; 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.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.UnixTime; import org.junit.Before; import org.junit.Test; @@ -32,67 +34,69 @@ import java.util.Arrays; import java.util.List; import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; -import static ch.dissem.bitmessage.utils.Singleton.security; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static ch.dissem.bitmessage.entity.payload.Pubkey.Feature.DOES_ACK; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; public class JdbcMessageRepositoryTest extends TestBase { private BitmessageAddress contactA; private BitmessageAddress contactB; private BitmessageAddress identity; - private TestJdbcConfig config; - private AddressRepository addressRepo; private MessageRepository repo; private Label inbox; private Label drafts; + private Label unread; @Before public void setUp() throws Exception { - config = new TestJdbcConfig(); + TestJdbcConfig config = new TestJdbcConfig(); config.reset(); - addressRepo = new JdbcAddressRepository(config); + AddressRepository addressRepo = new JdbcAddressRepository(config); repo = new JdbcMessageRepository(config); new InternalContext(new BitmessageContext.Builder() - .cryptography(security()) + .cryptography(cryptography()) .addressRepo(addressRepo) .messageRepo(repo) ); - BitmessageAddress tmp = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000)); + BitmessageAddress tmp = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000, DOES_ACK)); contactA = new BitmessageAddress(tmp.getAddress()); contactA.setPubkey(tmp.getPubkey()); addressRepo.save(contactA); contactB = new BitmessageAddress("BM-2cTtkBnb4BUYDndTKun6D9PjtueP2h1bQj"); addressRepo.save(contactB); - identity = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000)); + identity = new BitmessageAddress(new PrivateKey(false, 1, 1000, 1000, DOES_ACK)); addressRepo.save(identity); inbox = repo.getLabels(Label.Type.INBOX).get(0); drafts = repo.getLabels(Label.Type.DRAFT).get(0); + unread = repo.getLabels(Label.Type.UNREAD).get(0); - addMessage(contactA, identity, Plaintext.Status.RECEIVED, inbox); + addMessage(contactA, identity, Plaintext.Status.RECEIVED, inbox, unread); addMessage(identity, contactA, Plaintext.Status.DRAFT, drafts); - addMessage(identity, contactB, Plaintext.Status.DRAFT); + addMessage(identity, contactB, Plaintext.Status.DRAFT, unread); } @Test - public void testGetLabels() throws Exception { + public void ensureLabelsAreRetrieved() throws Exception { List<Label> labels = repo.getLabels(); assertEquals(5, labels.size()); } @Test - public void testGetLabelsByType() throws Exception { + public void ensureLabelsCanBeRetrievedByType() throws Exception { List<Label> labels = repo.getLabels(Label.Type.INBOX); assertEquals(1, labels.size()); assertEquals("Inbox", labels.get(0).toString()); } @Test - public void testFindMessagesByLabel() throws Exception { + public void ensureMessagesCanBeFoundByLabel() throws Exception { List<Plaintext> messages = repo.findMessages(inbox); assertEquals(1, messages.size()); Plaintext m = messages.get(0); @@ -101,6 +105,40 @@ public class JdbcMessageRepositoryTest extends TestBase { assertEquals(Plaintext.Status.RECEIVED, m.getStatus()); } + @Test + public void ensureUnreadMessagesCanBeFoundForAllLabels() { + int unread = repo.countUnread(null); + assertThat(unread, is(2)); + } + + @Test + public void ensureUnreadMessagesCanBeFoundByLabel() { + int unread = repo.countUnread(inbox); + assertThat(unread, is(1)); + } + + @Test + public void ensureMessageCanBeRetrievedByInitialHash() { + byte[] initialHash = new byte[64]; + Plaintext message = repo.findMessages(contactA).get(0); + message.setInitialHash(initialHash); + repo.save(message); + Plaintext other = repo.getMessage(initialHash); + assertThat(other, is(message)); + } + + @Test + public void ensureAckMessageCanBeUpdatedAndRetrieved() { + byte[] initialHash = new byte[64]; + Plaintext message = repo.findMessages(contactA).get(0); + message.setInitialHash(initialHash); + ObjectMessage ackMessage = message.getAckMessage(); + repo.save(message); + Plaintext other = repo.getMessage(initialHash); + assertThat(other, is(message)); + assertThat(other.getAckMessage(), is(ackMessage)); + } + @Test public void testFindMessagesByStatus() throws Exception { List<Plaintext> messages = repo.findMessages(Plaintext.Status.RECEIVED); @@ -124,7 +162,7 @@ public class JdbcMessageRepositoryTest extends TestBase { @Test public void testSave() throws Exception { Plaintext message = new Plaintext.Builder(MSG) - .IV(new InventoryVector(security().randomBytes(32))) + .IV(new InventoryVector(cryptography().randomBytes(32))) .from(identity) .to(contactA) .message("Subject", "Message") @@ -147,7 +185,7 @@ public class JdbcMessageRepositoryTest extends TestBase { public void testUpdate() throws Exception { List<Plaintext> messages = repo.findMessages(Plaintext.Status.DRAFT, contactA); Plaintext message = messages.get(0); - message.setInventoryVector(new InventoryVector(security().randomBytes(32))); + message.setInventoryVector(new InventoryVector(cryptography().randomBytes(32))); repo.save(message); messages = repo.findMessages(Plaintext.Status.DRAFT, contactA); @@ -156,11 +194,40 @@ public class JdbcMessageRepositoryTest extends TestBase { } @Test - public void testRemove() throws Exception { + public void ensureMessageIsRemoved() throws Exception { Plaintext toRemove = repo.findMessages(Plaintext.Status.DRAFT, contactB).get(0); - repo.remove(toRemove); List<Plaintext> messages = repo.findMessages(Plaintext.Status.DRAFT); - assertEquals(1, messages.size()); + assertEquals(2, messages.size()); + repo.remove(toRemove); + messages = repo.findMessages(Plaintext.Status.DRAFT); + assertThat(messages, hasSize(1)); + } + + @Test + public void ensureUnacknowledgedMessagesAreFoundForResend() throws Exception { + Plaintext message = new Plaintext.Builder(MSG) + .IV(new InventoryVector(cryptography().randomBytes(32))) + .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); + List<Plaintext> messagesToResend = repo.findMessagesToResend(); + assertThat(messagesToResend, hasSize(1)); + + message.updateNextTry(); + assertThat(message.getRetries(), is(2)); + assertThat(message.getNextTry(), greaterThan(UnixTime.now())); + repo.save(message); + messagesToResend = repo.findMessagesToResend(); + assertThat(messagesToResend, empty()); } private void addMessage(BitmessageAddress from, BitmessageAddress to, Plaintext.Status status, Label... labels) { diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcNodeRegistryTest.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcNodeRegistryTest.java index 044c44c..48ae664 100644 --- a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcNodeRegistryTest.java +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcNodeRegistryTest.java @@ -17,17 +17,25 @@ package ch.dissem.bitmessage.repository; import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; -import ch.dissem.bitmessage.ports.MemoryNodeRegistry; import ch.dissem.bitmessage.ports.NodeRegistry; +import ch.dissem.bitmessage.utils.UnixTime; import org.junit.Before; import org.junit.Test; import java.util.Arrays; +import java.util.Collections; import java.util.List; import static ch.dissem.bitmessage.utils.UnixTime.now; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +/** + * Please note that some tests fail if there is no internet connection, + * as the initial nodes' IP addresses are determined by DNS lookup. + */ public class JdbcNodeRegistryTest extends TestBase { private TestJdbcConfig config; private NodeRegistry registry; @@ -36,21 +44,26 @@ public class JdbcNodeRegistryTest extends TestBase { public void setUp() throws Exception { config = new TestJdbcConfig(); config.reset(); - registry = new MemoryNodeRegistry(); + registry = new JdbcNodeRegistry(config); registry.offerAddresses(Arrays.asList( - createAddress(1, 8444, 1, now()), - createAddress(2, 8444, 1, now()), - createAddress(3, 8444, 1, now()), - createAddress(4, 8444, 2, now()) + createAddress(1, 8444, 1, now()), + createAddress(2, 8444, 1, now()), + createAddress(3, 8444, 1, now()), + createAddress(4, 8444, 2, now()) )); } @Test - public void testInitNodes() throws Exception { + public void ensureGetKnownNodesWithoutStreamsYieldsEmpty() { + assertThat(registry.getKnownAddresses(10), empty()); + } + + @Test + public void ensurePredefinedNodeIsReturnedWhenDatabaseIsEmpty() throws Exception { config.reset(); List<NetworkAddress> knownAddresses = registry.getKnownAddresses(2, 1); - assertEquals(2, knownAddresses.size()); + assertEquals(1, knownAddresses.size()); } @Test @@ -65,16 +78,16 @@ public class JdbcNodeRegistryTest extends TestBase { @Test public void testOfferAddresses() throws Exception { registry.offerAddresses(Arrays.asList( - createAddress(1, 8444, 1, now()), - createAddress(10, 8444, 1, now()), - createAddress(11, 8444, 1, now()) + createAddress(1, 8444, 1, now()), + createAddress(10, 8444, 1, now()), + createAddress(11, 8444, 1, now()) )); List<NetworkAddress> knownAddresses = registry.getKnownAddresses(1000, 1); assertEquals(5, knownAddresses.size()); - registry.offerAddresses(Arrays.asList( - createAddress(1, 8445, 1, now()) + registry.offerAddresses(Collections.singletonList( + createAddress(1, 8445, 1, now()) )); knownAddresses = registry.getKnownAddresses(1000, 1); @@ -83,10 +96,10 @@ public class JdbcNodeRegistryTest extends TestBase { private NetworkAddress createAddress(int lastByte, int port, long stream, long time) { return new NetworkAddress.Builder() - .ipv6(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, lastByte) - .port(port) - .stream(stream) - .time(time) - .build(); + .ipv6(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, lastByte) + .port(port) + .stream(stream) + .time(time) + .build(); } -} \ No newline at end of file +} diff --git a/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepositoryTest.java b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepositoryTest.java new file mode 100644 index 0000000..4396eb5 --- /dev/null +++ b/repositories/src/test/java/ch/dissem/bitmessage/repository/JdbcProofOfWorkRepositoryTest.java @@ -0,0 +1,165 @@ +/* + * Copyright 2016 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.repository; + +import ch.dissem.bitmessage.BitmessageContext; +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.payload.GenericPayload; +import ch.dissem.bitmessage.entity.payload.GetPubkey; +import ch.dissem.bitmessage.ports.AddressRepository; +import ch.dissem.bitmessage.ports.MessageRepository; +import ch.dissem.bitmessage.ports.ProofOfWorkRepository.Item; +import ch.dissem.bitmessage.utils.TestUtils; +import ch.dissem.bitmessage.utils.UnixTime; +import org.junit.Before; +import org.junit.Test; + +import static ch.dissem.bitmessage.entity.Plaintext.Type.MSG; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; +import static ch.dissem.bitmessage.utils.UnixTime.MINUTE; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * @author Christian Basler + */ +public class JdbcProofOfWorkRepositoryTest extends TestBase { + private TestJdbcConfig config; + private JdbcProofOfWorkRepository repo; + private AddressRepository addressRepo; + private MessageRepository messageRepo; + + private byte[] initialHash1; + private byte[] initialHash2; + + @Before + public void setUp() throws Exception { + config = new TestJdbcConfig(); + config.reset(); + + addressRepo = new JdbcAddressRepository(config); + messageRepo = new JdbcMessageRepository(config); + repo = new JdbcProofOfWorkRepository(config); + InternalContext ctx = new InternalContext(new BitmessageContext.Builder() + .addressRepo(addressRepo) + .messageRepo(messageRepo) + .powRepo(repo) + .cryptography(cryptography()) + ); + + repo.putObject(new ObjectMessage.Builder() + .payload(new GetPubkey(new BitmessageAddress("BM-2DAjcCFrqFrp88FUxExhJ9kPqHdunQmiyn"))).build(), + 1000, 1000); + initialHash1 = repo.getItems().get(0); + + BitmessageAddress sender = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); + BitmessageAddress recipient = TestUtils.loadContact(); + addressRepo.save(sender); + addressRepo.save(recipient); + Plaintext plaintext = new Plaintext.Builder(MSG) + .ackData(cryptography().randomBytes(32)) + .from(sender) + .to(recipient) + .message("Subject", "Message") + .status(Plaintext.Status.DOING_PROOF_OF_WORK) + .build(); + messageRepo.save(plaintext); + initialHash2 = cryptography().getInitialHash(plaintext.getAckMessage()); + repo.putObject(new Item( + plaintext.getAckMessage(), + 1000, 1000, + UnixTime.now(+10 * MINUTE), + plaintext + )); + } + + @Test + public void ensureObjectIsStored() throws Exception { + int sizeBefore = repo.getItems().size(); + repo.putObject(new ObjectMessage.Builder() + .payload(new GetPubkey(new BitmessageAddress("BM-2D9U2hv3YBMHM1zERP32anKfVKohyPN9x2"))).build(), + 1000, 1000); + assertThat(repo.getItems().size(), is(sizeBefore + 1)); + } + + @Test + public void ensureAckObjectsAreStored() throws Exception { + int sizeBefore = repo.getItems().size(); + BitmessageAddress sender = TestUtils.loadIdentity("BM-2cSqjfJ8xK6UUn5Rw3RpdGQ9RsDkBhWnS8"); + BitmessageAddress recipient = TestUtils.loadContact(); + addressRepo.save(sender); + addressRepo.save(recipient); + Plaintext plaintext = new Plaintext.Builder(MSG) + .ackData(cryptography().randomBytes(32)) + .from(sender) + .to(recipient) + .message("Subject", "Message") + .status(Plaintext.Status.DOING_PROOF_OF_WORK) + .build(); + messageRepo.save(plaintext); + repo.putObject(new Item( + plaintext.getAckMessage(), + 1000, 1000, + UnixTime.now(+10 * MINUTE), + plaintext + )); + assertThat(repo.getItems().size(), is(sizeBefore + 1)); + } + + @Test + public void ensureItemCanBeRetrieved() { + Item item = repo.getItem(initialHash1); + assertThat(item, notNullValue()); + assertThat(item.object.getPayload(), instanceOf(GetPubkey.class)); + assertThat(item.nonceTrialsPerByte, is(1000L)); + assertThat(item.extraBytes, is(1000L)); + } + + @Test + public void ensureAckItemCanBeRetrieved() { + Item item = repo.getItem(initialHash2); + assertThat(item, notNullValue()); + assertThat(item.object.getPayload(), instanceOf(GenericPayload.class)); + assertThat(item.nonceTrialsPerByte, is(1000L)); + assertThat(item.extraBytes, is(1000L)); + assertThat(item.expirationTime, not(0)); + assertThat(item.message, notNullValue()); + assertThat(item.message.getFrom().getPrivateKey(), notNullValue()); + assertThat(item.message.getTo().getPubkey(), notNullValue()); + } + + @Test(expected = RuntimeException.class) + public void ensureRetrievingNonexistingItemThrowsException() { + repo.getItem(new byte[0]); + } + + @Test + public void ensureItemCanBeDeleted() { + repo.removeObject(initialHash1); + repo.removeObject(initialHash2); + assertTrue(repo.getItems().isEmpty()); + } + + @Test + public void ensureDeletionOfNonexistingItemIsHandledSilently() { + repo.removeObject(new byte[0]); + } +} diff --git a/wif/build.gradle b/wif/build.gradle index 93a0248..0c1ae14 100644 --- a/wif/build.gradle +++ b/wif/build.gradle @@ -13,7 +13,7 @@ uploadArchives { dependencies { compile project(':core') compile 'org.ini4j:ini4j:0.5.4' - testCompile 'junit:junit:4.11' + testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' testCompile project(':cryptography-bc') } diff --git a/wif/src/main/java/ch/dissem/bitmessage/wif/WifExporter.java b/wif/src/main/java/ch/dissem/bitmessage/wif/WifExporter.java index b41d645..3f4b370 100644 --- a/wif/src/main/java/ch/dissem/bitmessage/wif/WifExporter.java +++ b/wif/src/main/java/ch/dissem/bitmessage/wif/WifExporter.java @@ -18,6 +18,7 @@ package ch.dissem.bitmessage.wif; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.utils.Base58; import org.ini4j.Ini; import org.ini4j.Profile; @@ -26,7 +27,7 @@ import java.io.*; import java.util.Collection; import static ch.dissem.bitmessage.entity.valueobject.PrivateKey.PRIVATE_KEY_SIZE; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; /** * @author Christian Basler @@ -59,6 +60,9 @@ public class WifExporter { section.add("label", identity.getAlias()); section.add("enabled", true); section.add("decoy", false); + if (identity.isChan()) { + section.add("chan", identity.isChan()); + } section.add("noncetrialsperbyte", identity.getPubkey().getNonceTrialsPerByte()); section.add("payloadlengthextrabytes", identity.getPubkey().getExtraBytes()); section.add("privsigningkey", exportSecret(identity.getPrivateKey().getPrivateSigningKey())); @@ -73,7 +77,7 @@ public class WifExporter { byte[] result = new byte[37]; result[0] = (byte) 0x80; System.arraycopy(privateKey, 0, result, 1, PRIVATE_KEY_SIZE); - byte[] hash = security().doubleSha256(result, PRIVATE_KEY_SIZE + 1); + byte[] hash = cryptography().doubleSha256(result, PRIVATE_KEY_SIZE + 1); System.arraycopy(hash, 0, result, PRIVATE_KEY_SIZE + 1, 4); return Base58.encode(result); } @@ -95,7 +99,7 @@ public class WifExporter { try { ini.store(writer); } catch (IOException e) { - throw new RuntimeException(e); + throw new ApplicationException(e); } return writer.toString(); } diff --git a/wif/src/main/java/ch/dissem/bitmessage/wif/WifImporter.java b/wif/src/main/java/ch/dissem/bitmessage/wif/WifImporter.java index e88eaa4..f5a8ddc 100644 --- a/wif/src/main/java/ch/dissem/bitmessage/wif/WifImporter.java +++ b/wif/src/main/java/ch/dissem/bitmessage/wif/WifImporter.java @@ -23,8 +23,6 @@ import ch.dissem.bitmessage.factory.Factory; import ch.dissem.bitmessage.utils.Base58; import org.ini4j.Ini; import org.ini4j.Profile; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.*; import java.util.Arrays; @@ -33,17 +31,16 @@ import java.util.LinkedList; import java.util.List; import java.util.Map.Entry; -import static ch.dissem.bitmessage.utils.Singleton.security; +import static ch.dissem.bitmessage.utils.Singleton.cryptography; /** * @author Christian Basler */ public class WifImporter { - private final static Logger LOG = LoggerFactory.getLogger(WifImporter.class); + private static final byte WIF_FIRST_BYTE = (byte) 0x80; + private static final int WIF_SECRET_LENGTH = 37; private final BitmessageContext ctx; - private final Ini ini = new Ini(); - private final List<BitmessageAddress> identities = new LinkedList<>(); public WifImporter(BitmessageContext ctx, File file) throws IOException { @@ -73,6 +70,9 @@ public class WifImporter { section.get("payloadlengthextrabytes", long.class), Pubkey.Feature.bitfield(features) ); + if (section.containsKey("chan")) { + address.setChan(section.get("chan", boolean.class)); + } address.setAlias(section.get("label")); identities.add(address); } @@ -80,12 +80,14 @@ public class WifImporter { private byte[] getSecret(String walletImportFormat) throws IOException { byte[] bytes = Base58.decode(walletImportFormat); - if (bytes[0] != (byte) 0x80) - throw new IOException("Unknown format: 0x80 expected as first byte, but secret " + walletImportFormat + " was " + bytes[0]); - if (bytes.length != 37) - throw new IOException("Unknown format: 37 bytes expected, but secret " + walletImportFormat + " was " + bytes.length + " long"); + if (bytes[0] != WIF_FIRST_BYTE) + throw new IOException("Unknown format: 0x80 expected as first byte, but secret " + walletImportFormat + + " was " + bytes[0]); + if (bytes.length != WIF_SECRET_LENGTH) + throw new IOException("Unknown format: " + WIF_SECRET_LENGTH + + " bytes expected, but secret " + walletImportFormat + " was " + bytes.length + " long"); - byte[] hash = security().doubleSha256(bytes, 33); + byte[] hash = cryptography().doubleSha256(bytes, 33); for (int i = 0; i < 4; i++) { if (hash[i] != bytes[33 + i]) throw new IOException("Hash check failed for secret " + walletImportFormat); } diff --git a/wif/src/test/java/ch/dissem/bitmessage/wif/WifExporterTest.java b/wif/src/test/java/ch/dissem/bitmessage/wif/WifExporterTest.java index b930c0a..225f2f9 100644 --- a/wif/src/test/java/ch/dissem/bitmessage/wif/WifExporterTest.java +++ b/wif/src/test/java/ch/dissem/bitmessage/wif/WifExporterTest.java @@ -17,6 +17,7 @@ package ch.dissem.bitmessage.wif; import ch.dissem.bitmessage.BitmessageContext; +import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.ports.*; import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography; import org.junit.Before; @@ -80,9 +81,27 @@ public class WifExporterTest { "noncetrialsperbyte = 320" + System.lineSeparator() + "payloadlengthextrabytes = 14000" + System.lineSeparator() + "privsigningkey = 5KU2gbe9u4rKJ8PHYb1rvwMnZnAJj4gtV5GLwoYckeYzygWUzB9" + System.lineSeparator() + - "privencryptionkey = 5KHd4c6cavd8xv4kzo3PwnVaYuBgEfg7voPQ5V97aZKgpYBXGck" + System.lineSeparator() + System.lineSeparator(); + "privencryptionkey = 5KHd4c6cavd8xv4kzo3PwnVaYuBgEfg7voPQ5V97aZKgpYBXGck" + System.lineSeparator() + + System.lineSeparator(); importer = new WifImporter(ctx, expected); exporter.addIdentity(importer.getIdentities().get(0)); assertEquals(expected, exporter.toString()); } + + @Test + public void ensureChanIsAdded() throws Exception { + String expected = "[BM-2cW67GEKkHGonXKZLCzouLLxnLym3azS8r]" + System.lineSeparator() + + "label = general" + System.lineSeparator() + + "enabled = true" + System.lineSeparator() + + "decoy = false" + System.lineSeparator() + + "chan = true" + System.lineSeparator() + + "noncetrialsperbyte = 1000" + System.lineSeparator() + + "payloadlengthextrabytes = 1000" + System.lineSeparator() + + "privsigningkey = 5Jnbdwc4u4DG9ipJxYLznXSvemkRFueQJNHujAQamtDDoX3N1eQ" + System.lineSeparator() + + "privencryptionkey = 5JrDcFtQDv5ydcHRW6dfGUEvThoxCCLNEUaxQfy8LXXgTJzVAcq" + System.lineSeparator() + + System.lineSeparator(); + BitmessageAddress chan = ctx.joinChan("general", "BM-2cW67GEKkHGonXKZLCzouLLxnLym3azS8r"); + exporter.addIdentity(chan); + assertEquals(expected, exporter.toString()); + } } \ No newline at end of file diff --git a/wif/src/test/java/ch/dissem/bitmessage/wif/WifImporterTest.java b/wif/src/test/java/ch/dissem/bitmessage/wif/WifImporterTest.java index fe8e15c..398ff20 100644 --- a/wif/src/test/java/ch/dissem/bitmessage/wif/WifImporterTest.java +++ b/wif/src/test/java/ch/dissem/bitmessage/wif/WifImporterTest.java @@ -25,9 +25,7 @@ import org.junit.Test; import java.util.List; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.mockito.Matchers.any; +import static org.junit.Assert.*; import static org.mockito.Mockito.*; public class WifImporterTest { @@ -69,6 +67,7 @@ public class WifImporterTest { assertNotNull("Private key", identity.getPrivateKey()); assertEquals(32, identity.getPrivateKey().getPrivateEncryptionKey().length); assertEquals(32, identity.getPrivateKey().getPrivateSigningKey().length); + assertFalse(identity.isChan()); } @Test @@ -98,4 +97,20 @@ public class WifImporterTest { importer.importIdentity(identities.get(0)); verify(repo, times(1)).save(identities.get(0)); } + + @Test + public void ensureChanIsImported() throws Exception { + importer = new WifImporter(ctx, "[BM-2cW67GEKkHGonXKZLCzouLLxnLym3azS8r]\n" + + "label = [chan] general\n" + + "enabled = true\n" + + "decoy = false\n" + + "chan = true\n" + + "noncetrialsperbyte = 1000\n" + + "payloadlengthextrabytes = 1000\n" + + "privsigningkey = 5Jnbdwc4u4DG9ipJxYLznXSvemkRFueQJNHujAQamtDDoX3N1eQ\n" + + "privencryptionkey = 5JrDcFtQDv5ydcHRW6dfGUEvThoxCCLNEUaxQfy8LXXgTJzVAcq\n"); + assertEquals(1, importer.getIdentities().size()); + BitmessageAddress chan = importer.getIdentities().get(0); + assertTrue(chan.isChan()); + } } \ No newline at end of file