Server POW (work in progress, major refactoring)

This commit is contained in:
Christian Basler 2015-11-28 20:26:13 +01:00
parent 48ddfbbbd0
commit 891294f267
9 changed files with 386 additions and 178 deletions

2
.gitignore vendored
View File

@ -1,4 +1,6 @@
# Project specific files
admins.conf
clients.conf
*list.conf
config.properties
/*.db

View File

@ -44,6 +44,7 @@ dependencies {
compile 'ch.dissem.jabit:jabit-networking:0.2.1-SNAPSHOT'
compile 'ch.dissem.jabit:jabit-repositories:0.2.1-SNAPSHOT'
compile 'ch.dissem.jabit:jabit-security-bouncy:0.2.1-SNAPSHOT'
compile 'ch.dissem.jabit:jabit-extensions:0.2.1-SNAPSHOT'
compile 'com.h2database:h2:1.4.187'

View File

@ -56,7 +56,7 @@ public class JabitServerConfig {
.messageRepo(new JdbcMessageRepository(config))
.nodeRegistry(new MemoryNodeRegistry())
.networkHandler(new DefaultNetworkHandler())
.objectListener(new ServerObjectListener(admins(), clients(), whitelist(), shortlist(), blacklist()))
.listener(new ServerListener(admins(), clients(), whitelist(), shortlist(), blacklist()))
.security(new BouncySecurity())
.port(port)
.connectionLimit(connectionLimit)

View File

@ -0,0 +1,84 @@
/*
* 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.server;
import ch.dissem.bitmessage.entity.CustomMessage;
import ch.dissem.bitmessage.entity.MessagePayload;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.extensions.CryptoCustomMessage;
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest;
import ch.dissem.bitmessage.ports.CustomCommandHandler;
import ch.dissem.bitmessage.ports.ProofOfWorkEngine;
import ch.dissem.bitmessage.server.repository.ProofOfWorkRepository;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* @author Christian Basler
*/
public class ProofOfWorkRequestHandler implements CustomCommandHandler {
private final List<byte[]> decryptionKeys = new ArrayList<>();
private ProofOfWorkRepository repo;
private ProofOfWorkEngine engine;
@Override
public MessagePayload handle(CustomMessage message) {
try {
CryptoCustomMessage<ProofOfWorkRequest> cryptoMessage = CryptoCustomMessage.read(message.getData(),
(sender, in) -> ProofOfWorkRequest.read(sender, in));
ProofOfWorkRequest request = decrypt(cryptoMessage);
if (request == null) return error("Unknown encryption key.");
switch (request.getRequest()) {
case CALCULATE:
repo.storeTask(request); // FIXME
engine.calculateNonce(request.getInitialHash(), request.getData(), nonce -> {
});
}
return null;
} catch (IOException e) {
return error(e.getMessage());
}
}
private MessagePayload error(String message) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write("ERROR\n".getBytes("UTF-8"));
out.write(message.getBytes("UTF-8"));
return new CustomMessage(out.toByteArray());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private ProofOfWorkRequest decrypt(CryptoCustomMessage<ProofOfWorkRequest> cryptoMessage) {
for (byte[] key : decryptionKeys) {
try {
return cryptoMessage.decrypt(key);
} catch (DecryptionFailedException ignore) {
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return null;
}
}

View File

@ -0,0 +1,137 @@
/*
* 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.server;
import ch.dissem.bitmessage.BitmessageContext;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.Plaintext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Scanner;
import static ch.dissem.bitmessage.server.Constants.*;
/**
* @author Christian Basler
*/
public class ServerListener implements BitmessageContext.Listener {
private final static Logger LOG = LoggerFactory.getLogger(ServerListener.class);
private final Collection<BitmessageAddress> admins;
private final Collection<BitmessageAddress> clients;
private final Collection<String> whitelist;
private final Collection<String> shortlist;
private final Collection<String> blacklist;
public ServerListener(Collection<BitmessageAddress> admins,
Collection<BitmessageAddress> clients,
Collection<String> whitelist,
Collection<String> shortlist,
Collection<String> blacklist) {
this.admins = admins;
this.clients = clients;
this.whitelist = whitelist;
this.shortlist = shortlist;
this.blacklist = blacklist;
}
@Override
public void receive(Plaintext message) {
if (admins.contains(message.getFrom())) {
String[] command = message.getSubject().trim().toLowerCase().split("\\s+");
String data = message.getText();
if (command.length == 2) {
switch (command[1]) {
case "client":
case "clients":
updateUserList(CLIENT_LIST, clients, command[0], data);
break;
case "admin":
case "admins":
case "administrator":
case "administrators":
updateUserList(ADMIN_LIST, admins, command[0], data);
break;
case "whitelist":
updateList(WHITELIST, whitelist, command[0], data);
break;
case "shortlist":
updateList(SHORTLIST, shortlist, command[0], data);
break;
case "blacklist":
updateList(BLACKLIST, blacklist, command[0], data);
break;
default:
LOG.trace("ignoring unknown command " + message.getSubject());
}
}
}
}
private void updateUserList(String file, Collection<BitmessageAddress> list, String command, String data) {
switch (command) {
case "set":
list.clear();
case "add":
Scanner scanner = new Scanner(data);
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
try {
list.add(new BitmessageAddress(line));
} catch (Exception e) {
LOG.info(command + " " + file + ": ignoring line: " + line);
}
}
Utils.saveList(file, list.stream().map(BitmessageAddress::getAddress));
break;
case "remove":
list.removeIf(address -> data.contains(address.getAddress()));
Utils.saveList(file, list.stream().map(BitmessageAddress::getAddress));
break;
default:
LOG.info("unknown command " + command + " on list " + file);
}
}
private void updateList(String file, Collection<String> list, String command, String data) {
switch (command) {
case "set":
list.clear();
case "add":
Scanner scanner = new Scanner(data);
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
try {
list.add(new BitmessageAddress(line).getAddress());
} catch (Exception e) {
LOG.info(command + " " + file + ": ignoring line: " + line);
}
}
Utils.saveList(file, list.stream());
break;
case "remove":
list.removeIf(data::contains);
Utils.saveList(file, list.stream());
break;
default:
LOG.info("unknown command " + command + " on list " + file);
}
}
}

View File

@ -1,177 +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.server;
import ch.dissem.bitmessage.DefaultObjectListener;
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.exception.DecryptionFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.Scanner;
import static ch.dissem.bitmessage.factory.Factory.getObjectMessage;
import static ch.dissem.bitmessage.server.Constants.*;
import static ch.dissem.bitmessage.server.Utils.zero;
import static ch.dissem.bitmessage.utils.Singleton.security;
/**
* @author Christian Basler
*/
public class ServerObjectListener extends DefaultObjectListener {
private final static Logger LOG = LoggerFactory.getLogger(ServerObjectListener.class);
private final Collection<BitmessageAddress> admins;
private final Collection<BitmessageAddress> clients;
private final Collection<String> whitelist;
private final Collection<String> shortlist;
private final Collection<String> blacklist;
public ServerObjectListener(Collection<BitmessageAddress> admins, Collection<BitmessageAddress> clients, Collection<String> whitelist, Collection<String> shortlist, Collection<String> blacklist) {
super(p -> {
});
this.admins = admins;
this.clients = clients;
this.whitelist = whitelist;
this.shortlist = shortlist;
this.blacklist = blacklist;
}
@Override
protected void receive(ObjectMessage object, Broadcast broadcast) throws IOException {
processCommands(broadcast);
if (zero(object.getNonce())) {
calculateNonceForClient(object, broadcast);
} else {
super.receive(object, broadcast);
}
}
private void processCommands(Broadcast broadcast) throws IOException {
for (BitmessageAddress admin : admins) {
try {
broadcast.decrypt(admin);
Plaintext message = broadcast.getPlaintext();
String[] command = message.getSubject().trim().toLowerCase().split("\\s+");
String data = message.getText();
if (command.length == 2) {
switch (command[1]) {
case "client":
case "clients":
updateUserList(CLIENT_LIST, clients, command[0], data);
break;
case "admin":
case "admins":
case "administrator":
case "administrators":
updateUserList(ADMIN_LIST, admins, command[0], data);
break;
case "whitelist":
updateList(WHITELIST, whitelist, command[0], data);
break;
case "shortlist":
updateList(WHITELIST, shortlist, command[0], data);
break;
case "blacklist":
updateList(WHITELIST, blacklist, command[0], data);
break;
default:
LOG.trace("ignoring unknown command " + message.getSubject());
}
}
} catch (DecryptionFailedException ignore) {
}
}
}
private void calculateNonceForClient(ObjectMessage object, Broadcast broadcast) throws IOException {
// TODO: prevent doing calculation twice
for (BitmessageAddress client : clients) {
try {
broadcast.decrypt(client);
byte[] message = broadcast.getPlaintext().getMessage();
final ObjectMessage toRelay = getObjectMessage(3, new ByteArrayInputStream(message), message.length);
security().doProofOfWork(toRelay,
ctx.getNetworkNonceTrialsPerByte(), ctx.getNetworkExtraBytes(),
(nonce) -> {
toRelay.setNonce(nonce);
ctx.getInventory().storeObject(object);
ctx.getNetworkHandler().offer(object.getInventoryVector());
}
);
} catch (DecryptionFailedException ignore) {
}
}
}
private void updateUserList(String file, Collection<BitmessageAddress> list, String command, String data) {
switch (command) {
case "set":
list.clear();
case "add":
Scanner scanner = new Scanner(data);
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
try {
list.add(new BitmessageAddress(line));
} catch (Exception e) {
LOG.info(command + " " + file + ": ignoring line: " + line);
}
}
Utils.saveList(file, list.stream().map(BitmessageAddress::getAddress));
break;
case "remove":
list.removeIf(address -> data.contains(address.getAddress()));
Utils.saveList(file, list.stream().map(BitmessageAddress::getAddress));
break;
default:
LOG.info("unknown command " + command + " on list " + file);
}
}
private void updateList(String file, Collection<String> list, String command, String data) {
switch (command) {
case "set":
list.clear();
case "add":
Scanner scanner = new Scanner(data);
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
try {
list.add(new BitmessageAddress(line).getAddress());
} catch (Exception e) {
LOG.info(command + " " + file + ": ignoring line: " + line);
}
}
Utils.saveList(file, list.stream());
break;
case "remove":
list.removeIf(data::contains);
Utils.saveList(file, list.stream());
break;
default:
LOG.info("unknown command " + command + " on list " + file);
}
}
}

View File

@ -0,0 +1,30 @@
/*
* 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.server.entities;
/**
* @author Christian Basler
*/
public class Update<T> {
public final T oldValue;
public final T newValue;
public Update(T oldValue, T newValue) {
this.oldValue = oldValue;
this.newValue = newValue;
}
}

View File

@ -0,0 +1,124 @@
/*
* 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.server.repository;
import ch.dissem.bitmessage.InternalContext;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.valueobject.InventoryVector;
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest;
import ch.dissem.bitmessage.repository.JdbcConfig;
import ch.dissem.bitmessage.repository.JdbcHelper;
import ch.dissem.bitmessage.server.entities.Update;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Stream;
import static ch.dissem.bitmessage.server.repository.ProofOfWorkRepository.Status.*;
/**
* @author Christian Basler
*/
public class ProofOfWorkRepository extends JdbcHelper implements InternalContext.ContextHolder {
private static final Logger LOG = LoggerFactory.getLogger(ProofOfWorkRepository.class);
private InternalContext context;
protected ProofOfWorkRepository(JdbcConfig config) {
super(config);
}
@Override
public void setContext(InternalContext context) {
this.context = context;
}
/**
* client (can be removed once the new IV is returned)
* IV (without nonce)
* IV (with nonce, can be removed once the new IV is returned)
* status: calculating, finished, confirmed
* data (can be removed once POW calculation is done)
*/
public void storeTask(ProofOfWorkRequest request) {
try (Connection connection = config.getConnection()) {
PreparedStatement ps = connection.prepareStatement(
"INSERT INTO ProofOfWorkTask (initial_hash, client, target, status) VALUES (?, ?, ?, ?)");
ps.setBytes(1, request.getInitialHash());
ps.setString(2, request.getClient().getAddress());
ps.setBytes(3, request.getData());
ps.setString(4, CALCULATING.name());
ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void updateTask(InventoryVector temporaryIV, InventoryVector newIV) {
try (Connection connection = config.getConnection()) {
PreparedStatement ps = connection.prepareStatement(
"UPDATE ProofOfWorkTask SET IV = ?, status = ?, data = NULL WHERE temporaryIV = ?");
ps.setBytes(1, newIV.getHash());
ps.setString(2, FINISHED.name());
ps.setBytes(3, temporaryIV.getHash());
ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public Collection<Update<InventoryVector>> getUnconfirmed(BitmessageAddress client) {
List<Update<InventoryVector>> result = new LinkedList<>();
try (Connection connection = config.getConnection()) {
PreparedStatement ps = connection.prepareStatement("SELECT temporaryIV, IV FROM ProofOfWorkTask WHERE client = ? AND status = ?");
ps.setString(1, client.getAddress());
ps.setString(2, FINISHED.name());
ResultSet rs = ps.executeQuery();
while (rs.next()) {
InventoryVector temporaryIV = new InventoryVector(rs.getBytes(1));
InventoryVector iv = new InventoryVector(rs.getBytes(2));
result.add(new Update<>(temporaryIV, iv));
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return result;
}
public void confirm(Stream<InventoryVector> unconfirmed) {
try (Connection connection = config.getConnection()) {
PreparedStatement ps = connection.prepareStatement(
"UPDATE ProofOfWorkTask SET status = ?, IV = NULL, client = NULL WHERE IV = ANY(?)");
ps.setString(1, CONFIRMED.name());
ps.setArray(2, connection.createArrayOf("BINARY", unconfirmed.map(InventoryVector::getHash).toArray()));
ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public enum Status {
CALCULATING, FINISHED, CONFIRMED
}
}

View File

@ -0,0 +1,7 @@
CREATE TABLE ProofOfWorkTask (
initial_hash BINARY(64) NOT NULL PRIMARY KEY,
client VARCHAR(40) NOT NULL,
target BINARY(32),
nonce BINARY(8),
status VARCHAR(20),
);