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 # Project specific files
admins.conf
clients.conf
*list.conf *list.conf
config.properties config.properties
/*.db /*.db

View File

@ -44,6 +44,7 @@ dependencies {
compile 'ch.dissem.jabit:jabit-networking:0.2.1-SNAPSHOT' 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-repositories:0.2.1-SNAPSHOT'
compile 'ch.dissem.jabit:jabit-security-bouncy: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' compile 'com.h2database:h2:1.4.187'

View File

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