diff --git a/.gitignore b/.gitignore index 57462fb..0d42f5d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ # Project specific files +admins.conf +clients.conf *list.conf config.properties /*.db diff --git a/build.gradle b/build.gradle index 7f6b199..0fe15a1 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/src/main/java/ch/dissem/bitmessage/server/JabitServerConfig.java b/src/main/java/ch/dissem/bitmessage/server/JabitServerConfig.java index d0959f2..b3b314e 100644 --- a/src/main/java/ch/dissem/bitmessage/server/JabitServerConfig.java +++ b/src/main/java/ch/dissem/bitmessage/server/JabitServerConfig.java @@ -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) diff --git a/src/main/java/ch/dissem/bitmessage/server/ProofOfWorkRequestHandler.java b/src/main/java/ch/dissem/bitmessage/server/ProofOfWorkRequestHandler.java new file mode 100644 index 0000000..d5d95bd --- /dev/null +++ b/src/main/java/ch/dissem/bitmessage/server/ProofOfWorkRequestHandler.java @@ -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 decryptionKeys = new ArrayList<>(); + private ProofOfWorkRepository repo; + private ProofOfWorkEngine engine; + + @Override + public MessagePayload handle(CustomMessage message) { + try { + CryptoCustomMessage 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 cryptoMessage) { + for (byte[] key : decryptionKeys) { + try { + return cryptoMessage.decrypt(key); + } catch (DecryptionFailedException ignore) { + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return null; + } + +} diff --git a/src/main/java/ch/dissem/bitmessage/server/ServerListener.java b/src/main/java/ch/dissem/bitmessage/server/ServerListener.java new file mode 100644 index 0000000..228c6bc --- /dev/null +++ b/src/main/java/ch/dissem/bitmessage/server/ServerListener.java @@ -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 admins; + private final Collection clients; + + private final Collection whitelist; + private final Collection shortlist; + private final Collection blacklist; + + public ServerListener(Collection admins, + Collection clients, + Collection whitelist, + Collection shortlist, + Collection 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 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 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); + } + } +} diff --git a/src/main/java/ch/dissem/bitmessage/server/ServerObjectListener.java b/src/main/java/ch/dissem/bitmessage/server/ServerObjectListener.java deleted file mode 100644 index 656a8f5..0000000 --- a/src/main/java/ch/dissem/bitmessage/server/ServerObjectListener.java +++ /dev/null @@ -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 admins; - private final Collection clients; - - private final Collection whitelist; - private final Collection shortlist; - private final Collection blacklist; - - public ServerObjectListener(Collection admins, Collection clients, Collection whitelist, Collection shortlist, Collection 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 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 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); - } - } -} diff --git a/src/main/java/ch/dissem/bitmessage/server/entities/Update.java b/src/main/java/ch/dissem/bitmessage/server/entities/Update.java new file mode 100644 index 0000000..3aebada --- /dev/null +++ b/src/main/java/ch/dissem/bitmessage/server/entities/Update.java @@ -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 { + public final T oldValue; + public final T newValue; + + public Update(T oldValue, T newValue) { + this.oldValue = oldValue; + this.newValue = newValue; + } +} diff --git a/src/main/java/ch/dissem/bitmessage/server/repository/ProofOfWorkRepository.java b/src/main/java/ch/dissem/bitmessage/server/repository/ProofOfWorkRepository.java new file mode 100644 index 0000000..5db2215 --- /dev/null +++ b/src/main/java/ch/dissem/bitmessage/server/repository/ProofOfWorkRepository.java @@ -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> getUnconfirmed(BitmessageAddress client) { + List> 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 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 + } +} diff --git a/src/main/resources/db/migration/V1.0_ProofOfWorkTask.sql b/src/main/resources/db/migration/V1.0_ProofOfWorkTask.sql new file mode 100644 index 0000000..40d3135 --- /dev/null +++ b/src/main/resources/db/migration/V1.0_ProofOfWorkTask.sql @@ -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), +); \ No newline at end of file