Merge branch 'release/2.0.0'

This commit is contained in:
Christian Basler 2016-10-02 23:22:34 +02:00
commit e71f30736d
130 changed files with 6031 additions and 1976 deletions

7
.editorconfig Normal file
View File

@ -0,0 +1,7 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_size = 4

1
.gitignore vendored
View File

@ -5,6 +5,7 @@
### Gradle ###
.gradle
build/
classes/
# Ignore Gradle GUI config
gradle-app.setting

View File

@ -1,3 +1,10 @@
language: java
sudo: false # faster builds
jdk:
- oraclejdk8
before_install:
- pip install --user codecov
after_success:
- codecov

View File

@ -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();
```

View File

@ -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 {
}
}
}
}
jacocoTestReport {
reports {
xml.enabled = true
html.enabled = true
}
}
check.dependsOn jacocoTestReport
}

View File

@ -0,0 +1,57 @@
package ch.dissem.gradle
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* Sets the version as follows:
* <ul>
* <li>If the branch is 'master', the version is set to the latest tag (which is expected to be set by Git flow)</li>
* <li>Otherwise, the version is set to the branch name, with '-SNAPSHOT' appended</li>
* </ul>
*/
class GitFlowVersion implements Plugin<Project> {
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}'"
}
}
}

View File

@ -0,0 +1 @@
implementation-class=ch.dissem.gradle.GitFlowVersion

View File

@ -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')
}

View File

@ -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<BitmessageAddress> createDeterministicAddresses(
String passphrase, int numberOfAddresses, long version, long stream, boolean shorter) {
List<BitmessageAddress> 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.
* <p>
* You should call this method regularly, but be aware of the following:
* <ul>
* <li>As messages might be sent, POW will be done. It is therefore not advised to
* call it on shutdown.</li>
* <li>It shouldn't be called right after startup, as it's possible the missing
* acknowledgement was sent while the client was offline.</li>
* <li>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.</li>
* </ul>
*/
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.
* </p>
*
* @deprecated use {@link TTL#pubkey(long)} instead.
*/
public Builder pubkeyTTL(long days) {
if (days < 0 || days > 28 * DAY) throw new IllegalArgumentException("TTL must be between 1 and 28 days");
@ -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.");
}
};
}

View File

@ -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<Plaintext> 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());
}
}
}
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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();
}
<