diff --git a/.gitignore b/.gitignore index 6339218..3a97743 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,6 @@ out *.prefs *.project *.classpath -bulletin-board-server/local-instances/meerkat.db +*.db +*.sql +.arcconfig diff --git a/bulletin-board-client/build.gradle b/bulletin-board-client/build.gradle new file mode 100644 index 0000000..03c395a --- /dev/null +++ b/bulletin-board-client/build.gradle @@ -0,0 +1,242 @@ + +plugins { + id "us.kirchmeier.capsule" version "1.0.1" + id 'com.google.protobuf' version '0.7.0' +} + +apply plugin: 'java' +apply plugin: 'com.google.protobuf' +apply plugin: 'eclipse' +apply plugin: 'idea' + +apply plugin: 'maven-publish' + +// Is this a snapshot version? +ext { isSnapshot = false } + +ext { + groupId = 'org.factcenter.meerkat' + nexusRepository = "https://cs.idc.ac.il/nexus/content/groups/${isSnapshot ? 'unstable' : 'public'}/" + + // Credentials for IDC nexus repositories (needed only for using unstable repositories and publishing) + // Should be set in ${HOME}/.gradle/gradle.properties + nexusUser = project.hasProperty('nexusUser') ? project.property('nexusUser') : "" + nexusPassword = project.hasProperty('nexusPassword') ? project.property('nexusPassword') : "" +} + +description = "Meerkat Voting Common Library" + +// Your project version +version = "0.0" + +version += "${isSnapshot ? '-SNAPSHOT' : ''}" + + +dependencies { + + // Meerkat common + compile project(':meerkat-common') + compile project(':restful-api-common') + + // Jersey for RESTful API + compile 'org.glassfish.jersey.containers:jersey-container-servlet:2.22.+' + compile 'org.xerial:sqlite-jdbc:3.7.+' + + // Logging + compile 'org.slf4j:slf4j-api:1.7.7' + runtime 'ch.qos.logback:logback-classic:1.1.2' + runtime 'ch.qos.logback:logback-core:1.1.2' + + // Google protobufs + compile 'com.google.protobuf:protobuf-java:3.+' + + // Crypto + compile 'org.factcenter.qilin:qilin:1.1+' + compile 'org.bouncycastle:bcprov-jdk15on:1.53' + compile 'org.bouncycastle:bcpkix-jdk15on:1.53' + + // Depend on test resources from meerkat-common + testCompile project(path: ':meerkat-common', configuration: 'testOutput') + + // Depend on server compilation for the non-integration tests + testCompile project(path: ':bulletin-board-server') + + testCompile 'junit:junit:4.+' + testCompile 'org.hamcrest:hamcrest-all:1.3' + + runtime 'org.codehaus.groovy:groovy:2.4.+' +} + +test { + exclude '**/*IntegrationTest*' +// outputs.upToDateWhen { false } +} + +task integrationTest(type: Test) { + include '**/*IntegrationTest*' +// debug = true + outputs.upToDateWhen { false } + +} + +/*==== You probably don't have to edit below this line =======*/ + + +// Setup test configuration that can appear as a dependency in +// other subprojects +configurations { + testOutput.extendsFrom (testCompile) +} + +task testJar(type: Jar, dependsOn: testClasses) { + classifier = 'tests' + from sourceSets.test.output +} + +artifacts { + testOutput testJar +} + + + +// The run task added by the application plugin +// is also of type JavaExec. +tasks.withType(JavaExec) { + // Assign all Java system properties from + // the command line to the JavaExec task. + systemProperties System.properties +} + + +protobuf { + // Configure the protoc executable + protoc { + // Download from repositories + artifact = 'com.google.protobuf:protoc:3.+' + } +} + +idea { + module { + project.sourceSets.each { sourceSet -> + + def srcDir = "${protobuf.generatedFilesBaseDir}/$sourceSet.name/java" + + // add protobuf generated sources to generated source dir. + if ("test".equals(sourceSet.name)) { + testSourceDirs += file(srcDir) + } else { + sourceDirs += file(srcDir) + } + generatedSourceDirs += file(srcDir) + + } + + // Don't exclude build directory + excludeDirs -= file(buildDir) + } +} + +/*=================================== + * "Fat" Build targets + *===================================*/ + +if (project.hasProperty('mainClassName') && (mainClassName != null)) { + + task mavenCapsule(type: MavenCapsule) { + description = "Generate a capsule jar that automatically downloads and caches dependencies when run." + applicationClass mainClassName + destinationDir = buildDir + } + + task fatCapsule(type: FatCapsule) { + description = "Generate a single capsule jar containing everything. Use -Pfatmain=... to override main class" + + destinationDir = buildDir + + def fatMain = hasProperty('fatmain') ? fatmain : mainClassName + + applicationClass fatMain + + def testJar = hasProperty('test') + + if (hasProperty('fatmain')) { + appendix = "fat-${fatMain}" + } else { + appendix = "fat" + } + + if (testJar) { + from sourceSets.test.output + } + } + +} + +/*=================================== + * Repositories + *===================================*/ + +repositories { + + mavenLocal(); + + // Prefer the local nexus repository (it may have 3rd party artifacts not found in mavenCentral) + maven { + url nexusRepository + + if (isSnapshot) { + credentials { username + password + + username nexusUser + password nexusPassword + } + } + } + + // Use 'maven central' for other dependencies. + mavenCentral() +} + +task "info" << { + println "Project: ${project.name}" + println "Description: ${project.description}" + println "--------------------------" + println "GroupId: $groupId" + println "Version: $version (${isSnapshot ? 'snapshot' : 'release'})" + println "" +} +info.description 'Print some information about project parameters' + + +/*=================================== + * Publishing + *===================================*/ + +publishing { + publications { + mavenJava(MavenPublication) { + groupId project.groupId + pom.withXml { + asNode().appendNode('description', project.description) + } + from project.components.java + + } + } + repositories { + maven { + url "https://cs.idc.ac.il/nexus/content/repositories/${project.isSnapshot ? 'snapshots' : 'releases'}" + credentials { username + password + + username nexusUser + password nexusPassword + } + } + } +} + + + diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/BatchDataContainer.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BatchDataContainer.java new file mode 100644 index 0000000..1026529 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BatchDataContainer.java @@ -0,0 +1,23 @@ +package meerkat.bulletinboard; + +import meerkat.protobuf.BulletinBoardAPI.BatchChunk; +import meerkat.bulletinboard.AsyncBulletinBoardClient.BatchIdentifier; + +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 17-Jan-16. + * Used to store the complete data required for sending a batch data list inside a single object + */ +public class BatchDataContainer { + + public final MultiServerBatchIdentifier batchId; + public final List batchChunkList; + public final int startPosition; + + public BatchDataContainer(MultiServerBatchIdentifier batchId, List batchChunkList, int startPosition) { + this.batchId = batchId; + this.batchChunkList = batchChunkList; + this.startPosition = startPosition; + } +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientJob.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientJob.java deleted file mode 100644 index aca98d4..0000000 --- a/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientJob.java +++ /dev/null @@ -1,82 +0,0 @@ -package meerkat.bulletinboard; - -import com.google.protobuf.Message; - -import java.util.Collections; -import java.util.Iterator; -import java.util.List; - -/** - * Created by Arbel Deutsch Peled on 09-Dec-15. - * - * This class specifies the job that is required of a Bulletin Board Client Worker - */ -public class BulletinClientJob { - - public static enum JobType{ - POST_MESSAGE, // Post a message to servers - READ_MESSAGES, // Read messages according to some given filter (any server will do) - GET_REDUNDANCY // Check the redundancy of a specific message in the databases - } - - private List serverAddresses; - - private int minServers; // The minimal number of servers the job must be successful on for the job to be completed - - private final JobType jobType; - - private final Message payload; // The information associated with the job type - - private int maxRetry; // Number of retries for this job; set to -1 for infinite retries - - public BulletinClientJob(List serverAddresses, int minServers, JobType jobType, Message payload, int maxRetry) { - this.serverAddresses = serverAddresses; - this.minServers = minServers; - this.jobType = jobType; - this.payload = payload; - this.maxRetry = maxRetry; - } - - public void updateServerAddresses(List newServerAdresses) { - this.serverAddresses = newServerAdresses; - } - - public List getServerAddresses() { - return serverAddresses; - } - - public int getMinServers() { - return minServers; - } - - public JobType getJobType() { - return jobType; - } - - public Message getPayload() { - return payload; - } - - public int getMaxRetry() { - return maxRetry; - } - - public void shuffleAddresses() { - Collections.shuffle(serverAddresses); - } - - public void decMinServers(){ - minServers--; - } - - public void decMaxRetry(){ - if (maxRetry > 0) { - maxRetry--; - } - } - - public boolean isRetry(){ - return (maxRetry != 0); - } - -} \ No newline at end of file diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientJobResult.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientJobResult.java deleted file mode 100644 index be0501b..0000000 --- a/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientJobResult.java +++ /dev/null @@ -1,29 +0,0 @@ -package meerkat.bulletinboard; - -import com.google.protobuf.Message; - -/** - * Created by Arbel Deutsch Peled on 09-Dec-15. - * - * This class contains the end status and result of a Bulletin Board Client Job. - */ -public final class BulletinClientJobResult { - - private final BulletinClientJob job; // Stores the job the result refers to - - private final Message result; // The result of the job; valid only if success==true - - public BulletinClientJobResult(BulletinClientJob job, Message result) { - this.job = job; - this.result = result; - } - - public BulletinClientJob getJob() { - return job; - } - - public Message getResult() { - return result; - } - -} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java index 51599d5..1a4b62f 100644 --- a/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/BulletinClientWorker.java @@ -1,217 +1,38 @@ package meerkat.bulletinboard; -import com.google.protobuf.Message; -import meerkat.comm.CommunicationException; -import meerkat.crypto.Digest; -import meerkat.crypto.concrete.SHA256Digest; -import meerkat.protobuf.BulletinBoardAPI.*; -import meerkat.rest.Constants; -import meerkat.rest.ProtobufMessageBodyReader; -import meerkat.rest.ProtobufMessageBodyWriter; - -import javax.ws.rs.ProcessingException; -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.Response; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.Callable; - /** * Created by Arbel Deutsch Peled on 09-Dec-15. * - * This class implements the actual communication with the Bulletin Board Servers. + * This class handles bulletin client work. * It is meant to be used in a multi-threaded environment. */ -//TODO: Maybe make this abstract and inherit from it. -public class BulletinClientWorker implements Callable { +public abstract class BulletinClientWorker { - private final BulletinClientJob job; // The requested job to be handled + protected final IN payload; // Payload of the job - public BulletinClientWorker(BulletinClientJob job){ - this.job = job; + private int maxRetry; // Number of retries for this job; set to -1 for infinite retries + + public BulletinClientWorker(IN payload, int maxRetry) { + this.payload = payload; + this.maxRetry = maxRetry; } - // This resource enabled creation of a single Client per thread. - private static final ThreadLocal clientLocal = - new ThreadLocal () { - @Override protected Client initialValue() { - Client client; - client = ClientBuilder.newClient(); - client.register(ProtobufMessageBodyReader.class); - client.register(ProtobufMessageBodyWriter.class); + public IN getPayload() { + return payload; + } - return client; - } - }; - - // This resource enables creation of a single Digest per thread. - private static final ThreadLocal digestLocal = - new ThreadLocal () { - @Override protected Digest initialValue() { - Digest digest; - digest = new SHA256Digest(); //TODO: Make this generic. - - return digest; - } - }; - - /** - * This method carries out the actual communication with the servers via HTTP Post - * It accesses the servers according to the job it received and updates said job as it goes - * The method will only iterate once through the server list, removing servers from the list when they are no longer required - * In a POST_MESSAGE job: successful post to a server results in removing the server from the list - * In a GET_REDUNDANCY job: no server is removed from the list and the (absolute) number of servers in which the message was found is returned - * In a READ_MESSAGES job: successful retrieval from any server terminates the method and returns the received values; The list is not changed - * @return The original job, modified to fit the current state and the required output (if any) of the operation - * @throws IllegalArgumentException - * @throws CommunicationException - */ - public BulletinClientJobResult call() throws IllegalArgumentException, CommunicationException{ - - Client client = clientLocal.get(); - Digest digest = digestLocal.get(); - - WebTarget webTarget; - Response response; - - String requestPath; - Message msg; - - List serverAddresses = new LinkedList(job.getServerAddresses()); - - Message payload = job.getPayload(); - - BulletinBoardMessageList msgList; - - int count = 0; // Used to count number of servers which contain the required message in a GET_REDUNDANCY request. - - job.shuffleAddresses(); // This is done to randomize the order of access to servers primarily for READ operations - - // Prepare the request. - switch(job.getJobType()) { - - case POST_MESSAGE: - // Make sure the payload is a BulletinBoardMessage - if (!(payload instanceof BulletinBoardMessage)) { - throw new IllegalArgumentException("Cannot post an object that is not an instance of BulletinBoardMessage"); - } - - msg = payload; - requestPath = Constants.POST_MESSAGE_PATH; - break; - - case READ_MESSAGES: - // Make sure the payload is a MessageFilterList - if (!(payload instanceof MessageFilterList)) { - throw new IllegalArgumentException("Read failed: an instance of MessageFilterList is required as payload for a READ_MESSAGES operation"); - } - - msg = payload; - requestPath = Constants.READ_MESSAGES_PATH; - break; - - case GET_REDUNDANCY: - // Make sure the payload is a MessageId - if (!(payload instanceof MessageID)) { - throw new IllegalArgumentException("Cannot search for an object that is not an instance of MessageID"); - } - - requestPath = Constants.READ_MESSAGES_PATH; - - msg = MessageFilterList.newBuilder() - .addFilter(MessageFilter.newBuilder() - .setType(FilterType.MSG_ID) - .setId(((MessageID) payload).getID()) - .build() - ).build(); - - break; - - default: - throw new IllegalArgumentException("Unsupported job type"); - - } - - // Iterate through servers - - Iterator addressIterator = serverAddresses.iterator(); - - while (addressIterator.hasNext()) { - - // Send request to Server - String address = addressIterator.next(); - webTarget = client.target(address).path(Constants.BULLETIN_BOARD_SERVER_PATH).path(requestPath); - response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(msg, Constants.MEDIATYPE_PROTOBUF)); - - // Retrieve answer - switch(job.getJobType()) { - - case POST_MESSAGE: - try { - - response.readEntity(BoolMsg.class); // If a BoolMsg entity is returned: the post was successful - addressIterator.remove(); // Post to this server succeeded: remove server from list - job.decMinServers(); - - } catch (ProcessingException | IllegalStateException e) {} // Post to this server failed: retry next time - finally { - response.close(); - } - break; - - case GET_REDUNDANCY: - try { - msgList = response.readEntity(BulletinBoardMessageList.class); // If a BulletinBoardMessageList is returned: the read was successful - - if (msgList.getMessageList().size() > 0){ // Message was found in the server. - count++; - } - } catch (ProcessingException | IllegalStateException e) {} // Read failed: try with next server - finally { - response.close(); - } - break; - - case READ_MESSAGES: - try { - msgList = response.readEntity(BulletinBoardMessageList.class); // If a BulletinBoardMessageList is returned: the read was successful - return new BulletinClientJobResult(job, msgList); // Return the result - } catch (ProcessingException | IllegalStateException e) {} // Read failed: try with next server - finally { - response.close(); - } - break; - - } - - } - - // Return result (if haven't done so yet) - switch(job.getJobType()) { - - case POST_MESSAGE: - // The job now contains the information required to ascertain whether enough server posts have succeeded - // It will also contain the list of servers in which the post was not successful - job.updateServerAddresses(serverAddresses); - return new BulletinClientJobResult(job, null); - - case GET_REDUNDANCY: - // Return the number of servers in which the message was found - // The job now contains the list of these servers - return new BulletinClientJobResult(job, IntMsg.newBuilder().setValue(count).build()); - - case READ_MESSAGES: - // A successful operation would have already returned an output - // Therefore: no server access was successful - throw new CommunicationException("Could not access any server"); - - default: // This is required for successful compilation - throw new IllegalArgumentException("Unsupported job type"); + public int getMaxRetry() { + return maxRetry; + } + public void decMaxRetry(){ + if (maxRetry > 0) { + maxRetry--; } } + + public boolean isRetry(){ + return (maxRetry != 0); + } + } diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/CachedBulletinBoardClient.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/CachedBulletinBoardClient.java new file mode 100644 index 0000000..cdb86cb --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/CachedBulletinBoardClient.java @@ -0,0 +1,490 @@ +package meerkat.bulletinboard; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.protobuf.Timestamp; +import meerkat.comm.CommunicationException; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.protobuf.Crypto.Signature; +import meerkat.protobuf.Voting.*; + +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 03-Mar-16. + * This is a full-fledged implementation of a Bulletin Board Client + * It provides asynchronous access to several remote servers, as well as a local cache + * Read operations are performed on the local server + * Batch reads are performed on the local server and, if they fail, also on the remote servers + * Write operations are performed on the local server + * A Synchronizer is employed in order to keep the remote server up to date + * After any read is carried out, a subscription is made for the specific query to make sure the local DB will be updated + * The database also employs a synchronizer which makes sure local data is sent to the remote servers + */ +public class CachedBulletinBoardClient implements SubscriptionBulletinBoardClient { + + private final AsyncBulletinBoardClient localClient; + private final AsyncBulletinBoardClient remoteClient; + private final AsyncBulletinBoardClient queueClient; + private final BulletinBoardSubscriber subscriber; + private final BulletinBoardSynchronizer synchronizer; + + private Thread syncThread; + + private final static int DEFAULT_WAIT_CAP = 3000; + private final static int DEFAULT_SLEEP_INTERVAL = 3000; + + private class SubscriptionStoreCallback implements FutureCallback> { + + private final FutureCallback> callback; + + public SubscriptionStoreCallback(){ + callback = null; + } + + public SubscriptionStoreCallback(FutureCallback> callback){ + this.callback = callback; + } + + @Override + public void onSuccess(List result) { + for (BulletinBoardMessage msg : result) { + try { + + if (msg.getMsg().getDataTypeCase() == UnsignedBulletinBoardMessage.DataTypeCase.MSGID) { + + // This is a batch message: need to upload batch data as well as the message itself + BulletinBoardMessage completeMessage = localClient.readBatchData(msg); + + localClient.postMessage(completeMessage); + + } else { + + // This is a regular message: post it + localClient.postMessage(msg); + + } + + } catch (CommunicationException ignored) { + // TODO: log + } + } + } + + @Override + public void onFailure(Throwable t) { + if (callback != null) { + callback.onFailure(t); // This is some hard error that cannot be dealt with + } + } + + } + + /** + * Creates a Cached Client + * Assumes all parameters are initialized + * @param localClient is a Client for the local instance + * @param remoteClient is a Client for the remote instance(s); Should have endless retries for post operations + * @param subscriber is a subscription service to the remote instance(s) + * @param queueClient is a client for a local deletable server to be used as a queue for not-yet-uploaded messages + */ + public CachedBulletinBoardClient(AsyncBulletinBoardClient localClient, + AsyncBulletinBoardClient remoteClient, + BulletinBoardSubscriber subscriber, + DeletableSubscriptionBulletinBoardClient queueClient, + int sleepInterval, + int waitCap) { + + this.localClient = localClient; + this.remoteClient = remoteClient; + this.subscriber = subscriber; + this.queueClient = queueClient; + + this.synchronizer = new SimpleBulletinBoardSynchronizer(sleepInterval,waitCap); + synchronizer.init(queueClient, remoteClient); + syncThread = new Thread(synchronizer); + syncThread.start(); + } + + /** + * Creates a Cached Client + * Used default values foe the time caps + * */ + public CachedBulletinBoardClient(AsyncBulletinBoardClient localClient, + AsyncBulletinBoardClient remoteClient, + BulletinBoardSubscriber subscriber, + DeletableSubscriptionBulletinBoardClient queue) { + + this(localClient, remoteClient, subscriber, queue, DEFAULT_SLEEP_INTERVAL, DEFAULT_WAIT_CAP); + } + + @Override + public MessageID postMessage(final BulletinBoardMessage msg, final FutureCallback callback) { + + return localClient.postMessage(msg, new FutureCallback() { + @Override + public void onSuccess(Boolean result) { + remoteClient.postMessage(msg, callback); + } + + @Override + public void onFailure(Throwable t) { + if (callback != null) + callback.onFailure(t); + } + }); + + } + + @Override + public MessageID postAsBatch(final BulletinBoardMessage msg, final int chunkSize, final FutureCallback callback) { + + return localClient.postAsBatch(msg, chunkSize, new FutureCallback() { + @Override + public void onSuccess(Boolean result) { + remoteClient.postAsBatch(msg, chunkSize, callback); + } + + @Override + public void onFailure(Throwable t) { + if (callback != null) + callback.onFailure(t); + } + }); + + } + + @Override + public void beginBatch(final Iterable tags, final FutureCallback callback) { + + localClient.beginBatch(tags, new FutureCallback() { + + private BatchIdentifier localIdentifier; + + @Override + public void onSuccess(BatchIdentifier result) { + + localIdentifier = result; + + remoteClient.beginBatch(tags, new FutureCallback() { + @Override + public void onSuccess(BatchIdentifier result) { + if (callback != null) + callback.onSuccess(new CachedClientBatchIdentifier(localIdentifier, result)); + } + + @Override + public void onFailure(Throwable t) { + if (callback != null) + callback.onFailure(t); + } + }); + + } + + @Override + public void onFailure(Throwable t) { + if (callback != null) + callback.onFailure(t); + } + }); + + } + + @Override + public void postBatchData(final BatchIdentifier batchIdentifier, final List batchChunkList, + final int startPosition, final FutureCallback callback) throws IllegalArgumentException{ + + if (!(batchIdentifier instanceof CachedClientBatchIdentifier)){ + throw new IllegalArgumentException("Error: batch identifier supplied was not created by this class."); + } + + final CachedClientBatchIdentifier identifier = (CachedClientBatchIdentifier) batchIdentifier; + + localClient.postBatchData(identifier.getLocalIdentifier(), batchChunkList, startPosition, new FutureCallback() { + @Override + public void onSuccess(Boolean result) { + remoteClient.postBatchData(identifier.getRemoteIdentifier(), batchChunkList, startPosition, callback); + } + + @Override + public void onFailure(Throwable t) { + if (callback != null) + callback.onFailure(t); + } + }); + + } + + @Override + public void postBatchData(final BatchIdentifier batchIdentifier, final List batchChunkList, final FutureCallback callback) + throws IllegalArgumentException{ + + if (!(batchIdentifier instanceof CachedClientBatchIdentifier)){ + throw new IllegalArgumentException("Error: batch identifier supplied was not created by this class."); + } + + final CachedClientBatchIdentifier identifier = (CachedClientBatchIdentifier) batchIdentifier; + + localClient.postBatchData(identifier.getLocalIdentifier(), batchChunkList, new FutureCallback() { + @Override + public void onSuccess(Boolean result) { + remoteClient.postBatchData(identifier.getRemoteIdentifier(), batchChunkList, callback); + } + + @Override + public void onFailure(Throwable t) { + if (callback != null) + callback.onFailure(t); + } + }); + + } + + @Override + public void closeBatch(final BatchIdentifier batchIdentifier, final Timestamp timestamp, final Iterable signatures, + final FutureCallback callback) { + + if (!(batchIdentifier instanceof CachedClientBatchIdentifier)){ + throw new IllegalArgumentException("Error: batch identifier supplied was not created by this class."); + } + + final CachedClientBatchIdentifier identifier = (CachedClientBatchIdentifier) batchIdentifier; + + localClient.closeBatch(identifier.getLocalIdentifier(), timestamp, signatures, new FutureCallback() { + @Override + public void onSuccess(Boolean result) { + + remoteClient.closeBatch(identifier.getRemoteIdentifier(), timestamp, signatures, callback); + + } + + @Override + public void onFailure(Throwable t) { + if (callback != null) + callback.onFailure(t); + } + }); + + } + + @Override + public void getRedundancy(MessageID id, FutureCallback callback) { + + remoteClient.getRedundancy(id, callback); + + } + + @Override + public void readMessages(MessageFilterList filterList, final FutureCallback> callback) { + + localClient.readMessages(filterList, callback); + + subscriber.subscribe(filterList, new SubscriptionStoreCallback(callback)); + + } + + @Override + public void readMessage(final MessageID msgID, final FutureCallback callback) { + + localClient.readMessage(msgID, new FutureCallback() { + + @Override + public void onSuccess(BulletinBoardMessage result) { + if (callback != null) + callback.onSuccess(result); // Read from local client was successful + } + + @Override + public void onFailure(Throwable t) { + + // Read from local unsuccessful: try to read from remote + + remoteClient.readMessage(msgID, new FutureCallback() { + + @Override + public void onSuccess(BulletinBoardMessage result) { + + // Read from remote was successful: store in local and return result + + localClient.postMessage(result, null); + + if (callback != null) + callback.onSuccess(result); + + } + + @Override + public void onFailure(Throwable t) { + + // Read from remote was unsuccessful: report error + if (callback != null) + callback.onFailure(t); + + } + + }); + + } + + }); + + } + + @Override + public void readBatchData(final BulletinBoardMessage stub, final FutureCallback callback) throws IllegalArgumentException { + + localClient.readBatchData(stub, new FutureCallback() { + + @Override + public void onSuccess(BulletinBoardMessage result) { + if (callback != null) + callback.onSuccess(result); // Read from local client was successful + } + + @Override + public void onFailure(Throwable t) { + + // Read from local unsuccessful: try to read from remote + + remoteClient.readBatchData(stub, new FutureCallback() { + + @Override + public void onSuccess(BulletinBoardMessage result) { + + // Read from remote was successful: store in local and return result + + localClient.postMessage(result, null); + + if (callback != null) + callback.onSuccess(result); + + } + + @Override + public void onFailure(Throwable t) { + + // Read from remote was unsuccessful: report error + if (callback != null) + callback.onFailure(t); + + } + + }); + + } + + }); + + } + + @Override + public void querySync(SyncQuery syncQuery, FutureCallback callback) { + + localClient.querySync(syncQuery, callback); + + } + + @Override + /** + * This is a stub method + * All resources are assumed to be initialized + */ + public void init(BulletinBoardClientParams clientParams) {} + + @Override + public MessageID postMessage(BulletinBoardMessage msg) throws CommunicationException { + return localClient.postMessage(msg); + } + + @Override + public MessageID postAsBatch(BulletinBoardMessage msg, int chunkSize) throws CommunicationException { + MessageID result = localClient.postAsBatch(msg, chunkSize); + remoteClient.postAsBatch(msg, chunkSize); + return result; + } + + @Override + public float getRedundancy(MessageID id) throws CommunicationException { + return remoteClient.getRedundancy(id); + } + + @Override + public List readMessages(MessageFilterList filterList) throws CommunicationException { + subscriber.subscribe(filterList, new SubscriptionStoreCallback()); + return localClient.readMessages(filterList); + } + + @Override + public BulletinBoardMessage readMessage(MessageID msgID) throws CommunicationException { + + BulletinBoardMessage result = null; + try { + result = localClient.readMessage(msgID); + } catch (CommunicationException e) { + //TODO: log + } + + if (result == null){ + result = remoteClient.readMessage(msgID); + + if (result != null){ + localClient.postMessage(result); + } + + } + + return result; + + } + + @Override + public BulletinBoardMessage readBatchData(BulletinBoardMessage stub) throws CommunicationException, IllegalArgumentException { + + BulletinBoardMessage result = null; + try { + result = localClient.readBatchData(stub); + } catch (CommunicationException e) { + //TODO: log + } + + if (result == null){ + result = remoteClient.readBatchData(stub); + + if (result != null){ + localClient.postMessage(result); + } + + } + + return result; + + } + + @Override + public SyncQuery generateSyncQuery(GenerateSyncQueryParams generateSyncQueryParams) throws CommunicationException { + return localClient.generateSyncQuery(generateSyncQueryParams); + } + + @Override + public void close() { + localClient.close(); + remoteClient.close(); + synchronizer.stop(); + try { + syncThread.join(); + } catch (InterruptedException e) { + //TODO: log interruption + } + } + + @Override + public void subscribe(MessageFilterList filterList, FutureCallback> callback) { + subscriber.subscribe(filterList, new SubscriptionStoreCallback(callback)); + } + + @Override + public void subscribe(MessageFilterList filterList, long startEntry, FutureCallback> callback) { + subscriber.subscribe(filterList, startEntry, new SubscriptionStoreCallback(callback)); + } + +} \ No newline at end of file diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/CachedClientBatchIdentifier.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/CachedClientBatchIdentifier.java new file mode 100644 index 0000000..322473d --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/CachedClientBatchIdentifier.java @@ -0,0 +1,29 @@ +package meerkat.bulletinboard; + +import meerkat.bulletinboard.AsyncBulletinBoardClient.BatchIdentifier; + +import java.util.Arrays; + +/** + * Created by Arbel Deutsch Peled on 17-Jun-16. + */ +public final class CachedClientBatchIdentifier implements BatchIdentifier { + + // Per-server identifiers + private final BatchIdentifier localIdentifier; + private final BatchIdentifier remoteIdentifier; + + public CachedClientBatchIdentifier(BatchIdentifier localIdentifier, BatchIdentifier remoteIdentifier) { + this.localIdentifier = localIdentifier; + this.remoteIdentifier = remoteIdentifier; + } + + public BatchIdentifier getLocalIdentifier() { + return localIdentifier; + } + + public BatchIdentifier getRemoteIdentifier() { + return remoteIdentifier; + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/LocalBulletinBoardClient.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/LocalBulletinBoardClient.java new file mode 100644 index 0000000..2b904ed --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/LocalBulletinBoardClient.java @@ -0,0 +1,689 @@ +package meerkat.bulletinboard; + +import com.google.common.util.concurrent.*; +import com.google.protobuf.Int64Value; +import com.google.protobuf.Timestamp; +import meerkat.comm.CommunicationException; +import meerkat.comm.MessageInputStream; +import meerkat.comm.MessageInputStream.MessageInputStreamFactory; +import meerkat.comm.MessageOutputStream; +import meerkat.crypto.concrete.SHA256Digest; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.protobuf.Crypto.Signature; +import meerkat.protobuf.Voting.*; +import meerkat.util.BulletinBoardUtils; + +import javax.ws.rs.NotFoundException; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * Created by Arbel Deutsch Peled on 15-Mar-16. + * This client wraps a BulletinBoardServer in an asynchronous client. + * It is meant to be used as a local cache handler and for testing purposes. + * This means the access to the server is direct (via method calls) instead of through a TCP connection. + * The client implements both synchronous and asynchronous method calls, but calls to the server itself are performed synchronously. + */ +public class LocalBulletinBoardClient implements DeletableSubscriptionBulletinBoardClient { + + private final DeletableBulletinBoardServer server; + private final ListeningScheduledExecutorService executorService; + private final BulletinBoardDigest digest; + private final long subsrciptionDelay; + + /** + * Initializes an instance of the client + * @param server an initialized Bulletin Board Server instance which will perform the actual processing of the requests + * @param threadNum is the number of concurrent threads to allocate for the client + */ + public LocalBulletinBoardClient(DeletableBulletinBoardServer server, int threadNum, int subscriptionDelay) { + this.server = server; + this.executorService = MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(threadNum)); + this.digest = new GenericBulletinBoardDigest(new SHA256Digest()); + this.subsrciptionDelay = subscriptionDelay; + } + + private class MessagePoster implements Callable { + + private final BulletinBoardMessage msg; + + public MessagePoster(BulletinBoardMessage msg) { + this.msg = msg; + } + + + @Override + public Boolean call() throws CommunicationException { + return server.postMessage(msg).getValue(); + } + + } + + @Override + public MessageID postMessage(BulletinBoardMessage msg, FutureCallback callback) { + + Futures.addCallback(executorService.submit(new MessagePoster(msg)), callback); + + digest.update(msg.getMsg()); + return digest.digestAsMessageID(); + + } + + private class CompleteBatchPoster implements Callable { + + private final BulletinBoardMessage msg; + private final int chunkSize; + + public CompleteBatchPoster(BulletinBoardMessage msg, int chunkSize) { + this.msg = msg; + this.chunkSize = chunkSize; + } + + + @Override + public Boolean call() throws CommunicationException { + + BeginBatchMessage beginBatchMessage = BeginBatchMessage.newBuilder() + .addAllTag(msg.getMsg().getTagList()) + .build(); + + Int64Value batchId = server.beginBatch(beginBatchMessage); + + BatchMessage.Builder builder = BatchMessage.newBuilder() + .setBatchId(batchId.getValue()); + + List batchChunkList = BulletinBoardUtils.breakToBatch(msg, chunkSize); + + int i=0; + for (BatchChunk chunk : batchChunkList){ + + server.postBatchMessage(builder.setSerialNum(i).setData(chunk).build()); + + i++; + + } + + CloseBatchMessage closeBatchMessage = BulletinBoardUtils.generateCloseBatchMessage(batchId, batchChunkList.size(), msg); + + return server.closeBatch(closeBatchMessage).getValue(); + } + + } + + @Override + public MessageID postAsBatch(BulletinBoardMessage msg, int chunkSize, FutureCallback callback) { + + Futures.addCallback(executorService.submit(new CompleteBatchPoster(msg, chunkSize)), callback); + + digest.reset(); + digest.update(msg); + return digest.digestAsMessageID(); + + } + + private class BatchBeginner implements Callable { + + private final BeginBatchMessage msg; + + public BatchBeginner(BeginBatchMessage msg) { + this.msg = msg; + } + + + @Override + public SingleServerBatchIdentifier call() throws Exception { + return new SingleServerBatchIdentifier(server.beginBatch(msg)); + } + + } + + @Override + public void beginBatch(Iterable tags, FutureCallback callback) { + + BeginBatchMessage beginBatchMessage = BeginBatchMessage.newBuilder() + .addAllTag(tags) + .build(); + + Futures.addCallback(executorService.submit(new BatchBeginner(beginBatchMessage)), callback); + } + + private class BatchDataPoster implements Callable { + + private final SingleServerBatchIdentifier batchId; + private final List batchChunkList; + private final int startPosition; + + public BatchDataPoster(SingleServerBatchIdentifier batchId, List batchChunkList, int startPosition) { + this.batchId = batchId; + this.batchChunkList = batchChunkList; + this.startPosition = startPosition; + } + + + @Override + public Boolean call() throws Exception { + + BatchMessage.Builder msgBuilder = BatchMessage.newBuilder() + .setBatchId(batchId.getBatchId().getValue()); + + int i = startPosition; + for (BatchChunk data : batchChunkList){ + + msgBuilder.setSerialNum(i) + .setData(data); + + if (!server.postBatchMessage(msgBuilder.build()).getValue()) + return false; + + i++; + + } + + batchId.setLength(i); + + return true; + + } + + } + + @Override + public void postBatchData(BatchIdentifier batchId, List batchChunkList, int startPosition, FutureCallback callback) + throws IllegalArgumentException{ + + // Cast identifier to usable form + + if (!(batchId instanceof SingleServerBatchIdentifier)){ + throw new IllegalArgumentException("Error: batch identifier supplied was not created by this class."); + } + + SingleServerBatchIdentifier identifier = (SingleServerBatchIdentifier) batchId; + + // Add worker + + Futures.addCallback(executorService.submit(new BatchDataPoster(identifier, batchChunkList, startPosition)), callback); + + } + + @Override + public void postBatchData(BatchIdentifier batchId, List batchChunkList, FutureCallback callback) throws IllegalArgumentException{ + postBatchData(batchId, batchChunkList, 0, callback); + } + + + private class BatchCloser implements Callable { + + private final CloseBatchMessage msg; + + public BatchCloser(CloseBatchMessage msg) { + this.msg = msg; + } + + + @Override + public Boolean call() throws Exception { + return server.closeBatch(msg).getValue(); + } + + } + + @Override + public void closeBatch(BatchIdentifier batchId, Timestamp timestamp, Iterable signatures, FutureCallback callback) { + + // Cast identifier to usable form + + if (!(batchId instanceof SingleServerBatchIdentifier)){ + throw new IllegalArgumentException("Error: batch identifier supplied was not created by this class."); + } + + SingleServerBatchIdentifier identifier = (SingleServerBatchIdentifier) batchId; + + // Add worker + + CloseBatchMessage closeBatchMessage = CloseBatchMessage.newBuilder() + .setBatchId(identifier.getBatchId().getValue()) + .setBatchLength(identifier.getLength()) + .setTimestamp(timestamp) + .addAllSig(signatures) + .build(); + + Futures.addCallback(executorService.submit(new BatchCloser(closeBatchMessage)), callback); + + } + + private class RedundancyGetter implements Callable { + + private final MessageID msgId; + + public RedundancyGetter(MessageID msgId) { + this.msgId = msgId; + } + + + @Override + public Float call() throws Exception { + + MessageFilterList filterList = MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.MSG_ID) + .setId(msgId.getID()) + .build()) + .build(); + + ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); + MessageOutputStream outputStream = new MessageOutputStream<>(byteOutputStream); + server.readMessages(filterList,outputStream); + + MessageInputStream inputStream = + MessageInputStreamFactory.createMessageInputStream( + new ByteArrayInputStream(byteOutputStream.toByteArray()), + BulletinBoardMessage.class); + + if (inputStream.isAvailable()) + return 1.0f; + else + return 0.0f; + + } + + } + + @Override + public void getRedundancy(MessageID id, FutureCallback callback) { + Futures.addCallback(executorService.submit(new RedundancyGetter(id)), callback); + } + + private class MessageReader implements Callable> { + + private final MessageFilterList filterList; + + public MessageReader(MessageFilterList filterList) { + this.filterList = filterList; + } + + + @Override + public List call() throws Exception { + + ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); + MessageOutputStream outputStream = new MessageOutputStream<>(byteOutputStream); + server.readMessages(filterList, outputStream); + + MessageInputStream inputStream = + MessageInputStreamFactory.createMessageInputStream( + new ByteArrayInputStream(byteOutputStream.toByteArray()), + BulletinBoardMessage.class); + + return inputStream.asList(); + + } + + } + + @Override + public void readMessages(MessageFilterList filterList, FutureCallback> callback) { + Futures.addCallback(executorService.submit(new MessageReader(filterList)), callback); + } + + class SubscriptionCallback implements FutureCallback> { + + private MessageFilterList filterList; + private final FutureCallback> callback; + + public SubscriptionCallback(MessageFilterList filterList, FutureCallback> callback) { + this.filterList = filterList; + this.callback = callback; + } + + @Override + public void onSuccess(List result) { + + // Report new messages to user + if (callback != null) + callback.onSuccess(result); + + MessageFilterList.Builder filterBuilder = filterList.toBuilder(); + + // If any new messages arrived: update the MIN_ENTRY condition + if (result.size() > 0) { + + // Remove last filter from list (MIN_ENTRY one) + filterBuilder.removeFilter(filterBuilder.getFilterCount() - 1); + + // Add updated MIN_ENTRY filter (entry number is successor of last received entry's number) + filterBuilder.addFilter(MessageFilter.newBuilder() + .setType(FilterType.MIN_ENTRY) + .setEntry(result.get(result.size() - 1).getEntryNum() + 1) + .build()); + + } + + filterList = filterBuilder.build(); + + // Reschedule job + Futures.addCallback(executorService.schedule(new MessageReader(filterList), subsrciptionDelay, TimeUnit.MILLISECONDS), this); + + } + + @Override + public void onFailure(Throwable t) { + + // Notify caller about failure and terminate subscription + if (callback != null) + callback.onFailure(t); + + } + } + + @Override + public void subscribe(MessageFilterList filterList, long startEntry, FutureCallback> callback) { + + MessageFilterList subscriptionFilterList = + filterList.toBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.MIN_ENTRY) + .setEntry(startEntry) + .build()) + .build(); + + Futures.addCallback(executorService.submit(new MessageReader(subscriptionFilterList)), new SubscriptionCallback(subscriptionFilterList, callback)); + + } + + @Override + public void subscribe(MessageFilterList filterList, FutureCallback> callback) { + subscribe(filterList, 0, callback); + } + + private class BatchDataReader implements Callable> { + + private final MessageID msgID; + + public BatchDataReader(MessageID msgID) { + this.msgID = msgID; + } + + @Override + public List call() throws Exception { + + BatchQuery batchQuery = BatchQuery.newBuilder() + .setMsgID(msgID) + .setStartPosition(0) + .build(); + + ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); + MessageOutputStream batchOutputStream = new MessageOutputStream<>(byteOutputStream); + server.readBatch(batchQuery,batchOutputStream); + + MessageInputStream inputStream = + MessageInputStreamFactory.createMessageInputStream( + new ByteArrayInputStream(byteOutputStream.toByteArray()), + BatchChunk.class); + + return inputStream.asList(); + + } + } + + private class CompleteBatchReader implements Callable { + + private final MessageID msgID; + + public CompleteBatchReader(MessageID msgID) { + this.msgID = msgID; + } + + + @Override + public BulletinBoardMessage call() throws Exception { + + // Read message (mat be a stub) + + MessageFilterList filterList = MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.MSG_ID) + .setId(msgID.getID()) + .build()) + .build(); + + MessageReader messageReader = new MessageReader(filterList); + List bulletinBoardMessages = messageReader.call(); + + if (bulletinBoardMessages.size() <= 0) { + throw new NotFoundException("Message does not exist"); + } + + BulletinBoardMessage msg = bulletinBoardMessages.get(0); + + if (msg.getMsg().getDataTypeCase() == UnsignedBulletinBoardMessage.DataTypeCase.MSGID) { + + // Read data + + BatchDataReader batchDataReader = new BatchDataReader(msgID); + List batchChunkList = batchDataReader.call(); + + // Combine and return + + return BulletinBoardUtils.gatherBatch(msg, batchChunkList); + + } else { + return msg; + } + + } + + } + + private class BatchDataCombiner implements Callable { + + private final BulletinBoardMessage stub; + + public BatchDataCombiner(BulletinBoardMessage stub) { + this.stub = stub; + } + + @Override + public BulletinBoardMessage call() throws Exception { + + MessageID msgID = MessageID.newBuilder().setID(stub.getMsg().getMsgId()).build(); + + BatchDataReader batchDataReader = new BatchDataReader(msgID); + + List batchChunkList = batchDataReader.call(); + + return BulletinBoardUtils.gatherBatch(stub, batchChunkList); + + } + + } + + @Override + public void readMessage(MessageID msgID, FutureCallback callback) { + Futures.addCallback(executorService.submit(new CompleteBatchReader(msgID)), callback); + } + + @Override + public void readBatchData(BulletinBoardMessage stub, FutureCallback callback) throws IllegalArgumentException { + + if (stub.getMsg().getDataTypeCase() != UnsignedBulletinBoardMessage.DataTypeCase.MSGID){ + throw new IllegalArgumentException("Message is not a stub and does not contain the required message ID"); + } + + Futures.addCallback(executorService.submit(new BatchDataCombiner(stub)),callback); + + } + + private class SyncQueryHandler implements Callable { + + private final SyncQuery syncQuery; + + public SyncQueryHandler(SyncQuery syncQuery) { + this.syncQuery = syncQuery; + } + + + @Override + public SyncQueryResponse call() throws Exception { + return server.querySync(syncQuery); + } + + } + + @Override + public void querySync(SyncQuery syncQuery, FutureCallback callback) { + Futures.addCallback(executorService.submit(new SyncQueryHandler(syncQuery)), callback); + } + + /** + * This method is a stub, since the implementation only considers one server, and that is given in the constructor + * @param ignored is ignored + */ + @Override + public void init(BulletinBoardClientParams ignored) {} + + @Override + public MessageID postMessage(BulletinBoardMessage msg) throws CommunicationException { + + MessagePoster poster = new MessagePoster(msg); + poster.call(); + + digest.update(msg.getMsg()); + return digest.digestAsMessageID(); + + } + + @Override + public MessageID postAsBatch(BulletinBoardMessage msg, int chunkSize) throws CommunicationException { + + CompleteBatchPoster poster = new CompleteBatchPoster(msg, chunkSize); + Boolean result = poster.call(); + + if (!result) + throw new CommunicationException("Batch post failed"); + + digest.reset(); + digest.update(msg); + return digest.digestAsMessageID(); + + } + + @Override + public float getRedundancy(MessageID id) { + + try { + + RedundancyGetter getter = new RedundancyGetter(id); + return getter.call(); + + } catch (Exception e) { + return -1.0f; + } + + } + + @Override + public List readMessages(MessageFilterList filterList) throws CommunicationException{ + + try { + + MessageReader reader = new MessageReader(filterList); + return reader.call(); + + } catch (Exception e){ + throw new CommunicationException("Error reading from server"); + } + + } + + @Override + public BulletinBoardMessage readMessage(MessageID msgID) throws CommunicationException { + + MessageFilterList filterList = MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.MSG_ID) + .setId(msgID.getID()) + .build()) + .build(); + + CompleteBatchReader completeBatchReader = new CompleteBatchReader(msgID); + + try { + return completeBatchReader.call(); + } catch (Exception e) { + throw new CommunicationException(e.getMessage() + " " + e.getMessage()); + } + + } + + @Override + public BulletinBoardMessage readBatchData(BulletinBoardMessage stub) throws CommunicationException, IllegalArgumentException { + + if (stub.getMsg().getDataTypeCase() != UnsignedBulletinBoardMessage.DataTypeCase.MSGID){ + throw new IllegalArgumentException("Message is not a stub and does not contain the required message ID"); + } + + BatchDataCombiner combiner = new BatchDataCombiner(stub); + + try { + return combiner.call(); + } catch (Exception e) { + throw new CommunicationException(e.getCause() + " " + e.getMessage()); + } + + } + + @Override + public SyncQuery generateSyncQuery(GenerateSyncQueryParams generateSyncQueryParams) throws CommunicationException { + return server.generateSyncQuery(generateSyncQueryParams); + } + + @Override + public void deleteMessage(MessageID msgID, FutureCallback callback) { + + try { + Boolean deleted = server.deleteMessage(msgID).getValue(); + if (callback != null) + callback.onSuccess(deleted); + } catch (CommunicationException e) { + if (callback != null) + callback.onFailure(e); + } + + } + + @Override + public void deleteMessage(long entryNum, FutureCallback callback) { + + try { + Boolean deleted = server.deleteMessage(entryNum).getValue(); + if (callback != null) + callback.onSuccess(deleted); + } catch (CommunicationException e) { + if (callback != null) + callback.onFailure(e); + } + + } + + @Override + public boolean deleteMessage(MessageID msgID) throws CommunicationException { + return server.deleteMessage(msgID).getValue(); + } + + @Override + public boolean deleteMessage(long entryNum) throws CommunicationException { + return server.deleteMessage(entryNum).getValue(); + } + + @Override + public void close() { + try { + server.close(); + } catch (CommunicationException ignored) {} + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/MultiServerBatchIdentifier.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/MultiServerBatchIdentifier.java new file mode 100644 index 0000000..4934ee6 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/MultiServerBatchIdentifier.java @@ -0,0 +1,27 @@ +package meerkat.bulletinboard; + +import meerkat.bulletinboard.AsyncBulletinBoardClient.BatchIdentifier; + +import java.util.Arrays; + +/** + * Created by Arbel Deutsch Peled on 17-Jun-16. + */ +public final class MultiServerBatchIdentifier implements AsyncBulletinBoardClient.BatchIdentifier { + + // Per-server identifiers + private final Iterable identifiers; + + public MultiServerBatchIdentifier(Iterable identifiers) { + this.identifiers = identifiers; + } + + public MultiServerBatchIdentifier(BatchIdentifier[] identifiers) { + this.identifiers = Arrays.asList(identifiers); + } + + public Iterable getIdentifiers() { + return identifiers; + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/MultiServerWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/MultiServerWorker.java new file mode 100644 index 0000000..0073be2 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/MultiServerWorker.java @@ -0,0 +1,98 @@ +package meerkat.bulletinboard; + +import com.google.common.util.concurrent.FutureCallback; + +import com.google.common.util.concurrent.FutureCallback; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by Arbel Deutsch Peled on 09-Dec-15. + * + * This is a general class for handling multi-server work + * It utilizes Single Server Clients to perform the actual per-server work + */ +public abstract class MultiServerWorker extends BulletinClientWorker implements Runnable, FutureCallback{ + + protected final List clients; + + protected AtomicInteger minServers; // The minimal number of servers the job must be successful on for the job to be completed + + protected AtomicInteger maxFailedServers; // The maximal number of allowed server failures + + private AtomicBoolean returnedResult; + + private final FutureCallback futureCallback; + + /** + * Constructor + * @param clients contains a list of Single Server clients to handle requests + * @param shuffleClients is a boolean stating whether or not it is needed to shuffle the clients + * @param minServers is the minimal amount of servers needed in order to successfully complete the job + * @param payload is the payload for the job + * @param maxRetry is the maximal per-server retry count + * @param futureCallback contains the callback methods used to report the result back to the client + */ + public MultiServerWorker(List clients, boolean shuffleClients, + int minServers, IN payload, int maxRetry, + FutureCallback futureCallback) { + + super(payload,maxRetry); + + this.clients = clients; + if (shuffleClients){ + Collections.shuffle(clients); + } + + this.minServers = new AtomicInteger(minServers); + maxFailedServers = new AtomicInteger(clients.size() - minServers); + this.futureCallback = futureCallback; + + returnedResult = new AtomicBoolean(false); + + } + + /** + * Constructor overload without client shuffling + */ + public MultiServerWorker(List clients, + int minServers, IN payload, int maxRetry, + FutureCallback futureCallback) { + + this(clients, false, minServers, payload, maxRetry, futureCallback); + + } + + /** + * Used to report a successful operation to the client + * Only reports once to the client + * @param result is the result + */ + protected void succeed(OUT result){ + if (returnedResult.compareAndSet(false, true)) { + if (futureCallback != null) + futureCallback.onSuccess(result); + } + } + + /** + * Used to report a failed operation to the client + * Only reports once to the client + * @param t contains the error/exception that occurred + */ + protected void fail(Throwable t){ + if (returnedResult.compareAndSet(false, true)) { + if (futureCallback != null) + futureCallback.onFailure(t); + } + } + + protected int getClientNumber() { + return clients.size(); + } + +} \ No newline at end of file diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardClient.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardClient.java index f340cae..a274f53 100644 --- a/bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardClient.java +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardClient.java @@ -1,13 +1,16 @@ package meerkat.bulletinboard; +import com.google.protobuf.BoolValue; import com.google.protobuf.ByteString; +import com.google.protobuf.Int64Value; +import meerkat.bulletinboard.workers.singleserver.*; import meerkat.comm.CommunicationException; -import meerkat.crypto.Digest; import meerkat.crypto.concrete.SHA256Digest; +import meerkat.protobuf.BulletinBoardAPI; import meerkat.protobuf.BulletinBoardAPI.*; -import meerkat.protobuf.Voting; -import meerkat.protobuf.Voting.BulletinBoardClientParams; +import meerkat.protobuf.Voting.*; import meerkat.rest.*; +import meerkat.util.BulletinBoardUtils; import java.util.List; @@ -17,31 +20,35 @@ import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Response; +import static meerkat.bulletinboard.BulletinBoardConstants.*; + /** * Created by Arbel Deutsch Peled on 05-Dec-15. + * Implements BulletinBoardClient interface in a simple, straightforward manner */ -public class SimpleBulletinBoardClient{ //implements BulletinBoardClient { +public class SimpleBulletinBoardClient implements BulletinBoardClient{ - private List meerkatDBs; + protected List meerkatDBs; - private Client client; + protected Client client; - private Digest digest; + protected BulletinBoardDigest digest; /** * Stores database locations and initializes the web Client * @param clientParams contains the data needed to access the DBs */ -// @Override - public void init(Voting.BulletinBoardClientParams clientParams) { + @Override + public void init(BulletinBoardClientParams clientParams) { - meerkatDBs = clientParams.getBulletinBoardAddressList(); + this.meerkatDBs = clientParams.getBulletinBoardAddressList(); client = ClientBuilder.newClient(); client.register(ProtobufMessageBodyReader.class); client.register(ProtobufMessageBodyWriter.class); - digest = new SHA256Digest(); + // Wrap the Digest into a BatchDigest + digest = new GenericBulletinBoardDigest(new SHA256Digest()); } @@ -52,23 +59,20 @@ public class SimpleBulletinBoardClient{ //implements BulletinBoardClient { * @return the message ID for later retrieval * @throws CommunicationException */ -// @Override + @Override public MessageID postMessage(BulletinBoardMessage msg) throws CommunicationException { WebTarget webTarget; - Response response; + Response response = null; // Post message to all databases try { for (String db : meerkatDBs) { - webTarget = client.target(db).path(Constants.BULLETIN_BOARD_SERVER_PATH).path(Constants.POST_MESSAGE_PATH); - response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(msg, Constants.MEDIATYPE_PROTOBUF)); - // Only consider valid responses - if (response.getStatusInfo() == Response.Status.OK - || response.getStatusInfo() == Response.Status.CREATED) { - response.readEntity(BoolMsg.class).getValue(); - } + SingleServerPostMessageWorker worker = new SingleServerPostMessageWorker(db, msg, 0); + + worker.call(); + } } catch (Exception e) { // Occurs only when server replies with valid status but invalid data throw new CommunicationException("Error accessing database: " + e.getMessage()); @@ -88,7 +92,7 @@ public class SimpleBulletinBoardClient{ //implements BulletinBoardClient { * @param id is the requested message ID * @return the number of DBs in which retrieval was successful */ -// @Override + @Override public float getRedundancy(MessageID id) { WebTarget webTarget; Response response; @@ -104,7 +108,7 @@ public class SimpleBulletinBoardClient{ //implements BulletinBoardClient { for (String db : meerkatDBs) { try { - webTarget = client.target(db).path(Constants.BULLETIN_BOARD_SERVER_PATH).path(Constants.READ_MESSAGES_PATH); + webTarget = client.target(db).path(BULLETIN_BOARD_SERVER_PATH).path(READ_MESSAGES_PATH); response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(filterList, Constants.MEDIATYPE_PROTOBUF)); @@ -123,39 +127,203 @@ public class SimpleBulletinBoardClient{ //implements BulletinBoardClient { * If at the operation is successful for some DB: return the results and stop iterating * If no operation is successful: return null (NOT blank list) * @param filterList return only messages that match the filters (null means no filtering). - * @return + * @return the list of Bulletin Board messages that are returned from a server */ -// @Override - public List readMessages(MessageFilterList filterList) { - WebTarget webTarget; - Response response; - BulletinBoardMessageList messageList; + @Override + public List readMessages(MessageFilterList filterList) throws CommunicationException{ // Replace null filter list with blank one. if (filterList == null){ - filterList = MessageFilterList.newBuilder().build(); + filterList = MessageFilterList.getDefaultInstance(); } + String exceptionString = ""; + for (String db : meerkatDBs) { + try { - webTarget = client.target(db).path(Constants.BULLETIN_BOARD_SERVER_PATH).path(Constants.READ_MESSAGES_PATH); - response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(filterList, Constants.MEDIATYPE_PROTOBUF)); + SingleServerReadMessagesWorker worker = new SingleServerReadMessagesWorker(db, filterList, 0); - messageList = response.readEntity(BulletinBoardMessageList.class); + List result = worker.call(); - if (messageList != null){ - return messageList.getMessageList(); - } + return result; - } catch (Exception e) {} + } catch (Exception e) { + //TODO: log + exceptionString += e.getMessage() + "\n"; + } } - return null; + throw new CommunicationException("Could not find message in any DB. Errors follow:\n" + exceptionString); + + } + + @Override + public MessageID postAsBatch(BulletinBoardMessage msg, int chunkSize) throws CommunicationException { + + List chunkList = BulletinBoardUtils.breakToBatch(msg, chunkSize); + + BeginBatchMessage beginBatchMessage = BulletinBoardUtils.generateBeginBatchMessage(msg); + + boolean posted = false; + + // Post message to all databases + + for (String db : meerkatDBs) { + + try { + + int pos = 0; + + SingleServerBeginBatchWorker beginBatchWorker = new SingleServerBeginBatchWorker(db, beginBatchMessage, 0); + + Int64Value batchId = beginBatchWorker.call(); + + BatchMessage.Builder builder = BatchMessage.newBuilder().setBatchId(batchId.getValue()); + + for (BatchChunk batchChunk : chunkList) { + + SingleServerPostBatchWorker postBatchWorker = + new SingleServerPostBatchWorker( + db, + builder.setData(batchChunk).setSerialNum(pos).build(), + 0); + + postBatchWorker.call(); + + pos++; + + } + + CloseBatchMessage closeBatchMessage = BulletinBoardUtils.generateCloseBatchMessage(batchId, chunkList.size(), msg); + + SingleServerCloseBatchWorker closeBatchWorker = new SingleServerCloseBatchWorker(db, closeBatchMessage, 0); + + closeBatchWorker.call(); + + posted = true; + + } catch(Exception ignored) {} + + } + + if (!posted){ + throw new CommunicationException("Could not post to any server"); + } + + digest.reset(); + digest.update(msg); + return digest.digestAsMessageID(); + + } + + @Override + public BulletinBoardMessage readMessage(MessageID msgID) throws CommunicationException { + + MessageFilterList filterList = MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.MSG_ID) + .setId(msgID.getID()) + .build()) + .build(); + + BatchQuery batchQuery = BatchQuery.newBuilder() + .setMsgID(msgID) + .setStartPosition(0) + .build(); + + String exceptionString = ""; + + for (String db : meerkatDBs) { + + try { + SingleServerReadMessagesWorker messagesWorker = new SingleServerReadMessagesWorker(db, filterList, 0); + + List messages = messagesWorker.call(); + + if (messages == null || messages.size() < 1) + continue; + + BulletinBoardMessage stub = messages.get(0); + + SingleServerReadBatchWorker batchWorker = new SingleServerReadBatchWorker(db, batchQuery, 0); + + List batchChunkList = batchWorker.call(); + + return BulletinBoardUtils.gatherBatch(stub, batchChunkList); + + } catch (Exception e) { + //TODO: log + exceptionString += e.getMessage() + "\n"; + } + } + + throw new CommunicationException("Could not find message in any DB. Errors follow:\n" + exceptionString); + + } + + @Override + public BulletinBoardMessage readBatchData(BulletinBoardMessage stub) throws CommunicationException, IllegalArgumentException { + + if (stub.getMsg().getDataTypeCase() != UnsignedBulletinBoardMessage.DataTypeCase.MSGID){ + throw new IllegalArgumentException("Message is not a stub and does not contain the required message ID"); + } + + BatchQuery batchQuery = BatchQuery.newBuilder() + .setMsgID(MessageID.newBuilder() + .setID(stub.getMsg().getMsgId()) + .build()) + .setStartPosition(0) + .build(); + + String exceptionString = ""; + + for (String db : meerkatDBs) { + + try { + + SingleServerReadBatchWorker batchWorker = new SingleServerReadBatchWorker(db, batchQuery, 0); + + List batchChunkList = batchWorker.call(); + + return BulletinBoardUtils.gatherBatch(stub, batchChunkList); + + } catch (Exception e) { + //TODO: log + exceptionString += e.getMessage() + "\n"; + } + } + + throw new CommunicationException("Could not find message in any DB. Errors follow:\n" + exceptionString); + + } + + @Override + public SyncQuery generateSyncQuery(GenerateSyncQueryParams generateSyncQueryParams) throws CommunicationException { + + WebTarget webTarget; + Response response; + + for (String db : meerkatDBs) { + + try { + webTarget = client.target(db).path(BULLETIN_BOARD_SERVER_PATH).path(GENERATE_SYNC_QUERY_PATH); + + response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(generateSyncQueryParams, Constants.MEDIATYPE_PROTOBUF)); + + return response.readEntity(SyncQuery.class); + + } catch (Exception e) {} + + } + + throw new CommunicationException("Could not contact any server"); + + } + + public void close() { + client.close(); } -// @Override -// public void registerNewMessageCallback(MessageCallback callback, MessageFilterList filterList) { -// callback.handleNewMessage(readMessages(filterList)); -// } } diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardSynchronizer.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardSynchronizer.java new file mode 100644 index 0000000..5cdf73b --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/SimpleBulletinBoardSynchronizer.java @@ -0,0 +1,241 @@ +package meerkat.bulletinboard; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.protobuf.ByteString; +import meerkat.comm.CommunicationException; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.util.BulletinBoardUtils; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Created by Arbel on 13/04/2016. + * Simple, straightforward implementation of the {@link BulletinBoardSynchronizer} interface + */ +public class SimpleBulletinBoardSynchronizer implements BulletinBoardSynchronizer { + + private DeletableSubscriptionBulletinBoardClient localClient; + private AsyncBulletinBoardClient remoteClient; + + private AtomicBoolean running; + private volatile SyncStatus syncStatus; + + private List> messageCountCallbacks; + private List> syncStatusCallbacks; + + private static final MessageFilterList EMPTY_FILTER = MessageFilterList.getDefaultInstance(); + private static final int DEFAULT_SLEEP_INTERVAL = 10000; // 10 Seconds + private static final int DEFAULT_WAIT_CAP = 300000; // 5 minutes wait before deciding that the sync has failed fatally + + private final int SLEEP_INTERVAL; + private final int WAIT_CAP; + + private Semaphore semaphore; + + private class SyncCallback implements FutureCallback> { + + @Override + public void onSuccess(List result) { + + // Notify Message Count callbacks if needed + + if (syncStatus != SyncStatus.SYNCHRONIZED || result.size() > 0) { + + for (FutureCallback callback : messageCountCallbacks){ + callback.onSuccess(result.size()); + } + + } + + // Handle upload and status change + + SyncStatus newStatus = SyncStatus.PENDING; + + if (result.size() == 0) { + newStatus = SyncStatus.SYNCHRONIZED; + semaphore.release(); + } + + else{ // Upload messages + + for (BulletinBoardMessage message : result){ + + try { + + if (message.getMsg().getDataTypeCase() == UnsignedBulletinBoardMessage.DataTypeCase.MSGID) { + + // This is a batch message: need to upload batch data as well as the message itself + + BulletinBoardMessage completeMsg = localClient.readBatchData(message); + + remoteClient.postMessage(completeMsg); + + localClient.deleteMessage(completeMsg.getEntryNum()); + + + } else { + + // This is a regular message: post it + remoteClient.postMessage(message); + + localClient.deleteMessage(message.getEntryNum()); + + } + + } catch (CommunicationException e) { + // This is an error with the local server + // TODO: log + updateSyncStatus(SyncStatus.SERVER_ERROR); + } + + } + + } + + updateSyncStatus(newStatus); + + } + + @Override + public void onFailure(Throwable t) { + + updateSyncStatus(SyncStatus.SERVER_ERROR); + + } + + } + + public SimpleBulletinBoardSynchronizer(int sleepInterval, int waitCap) { + this.syncStatus = SyncStatus.STOPPED; + this.SLEEP_INTERVAL = sleepInterval; + this.WAIT_CAP = waitCap; + this.running = new AtomicBoolean(false); + } + + public SimpleBulletinBoardSynchronizer() { + this(DEFAULT_SLEEP_INTERVAL, DEFAULT_WAIT_CAP); + } + + private synchronized void updateSyncStatus(SyncStatus newStatus) { + + if (!running.get()) { + + newStatus = SyncStatus.STOPPED; + + } + + if (newStatus != syncStatus){ + + syncStatus = newStatus; + + for (FutureCallback callback : syncStatusCallbacks){ + if (callback != null) + callback.onSuccess(syncStatus); + } + + } + + } + + @Override + public void init(DeletableSubscriptionBulletinBoardClient localClient, AsyncBulletinBoardClient remoteClient) { + + updateSyncStatus(SyncStatus.STOPPED); + + this.localClient = localClient; + this.remoteClient = remoteClient; + + messageCountCallbacks = new LinkedList<>(); + syncStatusCallbacks = new LinkedList<>(); + + semaphore = new Semaphore(0); + + } + + @Override + public SyncStatus getSyncStatus() { + return syncStatus; + } + + @Override + public void subscribeToSyncStatus(FutureCallback callback) { + syncStatusCallbacks.add(callback); + } + + @Override + public List getRemainingMessages() throws CommunicationException{ + return localClient.readMessages(EMPTY_FILTER); + } + + @Override + public void getRemainingMessages(FutureCallback> callback) { + localClient.readMessages(EMPTY_FILTER, callback); + } + + @Override + public long getRemainingMessagesCount() throws CommunicationException { + return localClient.readMessages(EMPTY_FILTER).size(); + } + + @Override + public void subscribeToRemainingMessagesCount(FutureCallback callback) { + messageCountCallbacks.add(callback); + } + + @Override + public void run() { + + if (running.compareAndSet(false,true)){ + + updateSyncStatus(SyncStatus.PENDING); + SyncCallback callback = new SyncCallback(); + + while (syncStatus != SyncStatus.STOPPED) { + + do { + + localClient.readMessages(EMPTY_FILTER, callback); + + try { + + semaphore.tryAcquire(WAIT_CAP, TimeUnit.MILLISECONDS); + //TODO: log hard error. Too much time trying to upload data. + + } catch (InterruptedException ignored) { + // We expect an interruption when the upload will complete + } + + } while (syncStatus == SyncStatus.PENDING); + + // Database is synced. Wait for new data. + + try { + semaphore.tryAcquire(SLEEP_INTERVAL, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + //TODO: log (probably nudged) + } + + } + + } + + } + + @Override + public void nudge() { + semaphore.release(); + } + + @Override + public void stop() { + + running.set(false); + updateSyncStatus(SyncStatus.STOPPED); + + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/SingleServerBatchIdentifier.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/SingleServerBatchIdentifier.java new file mode 100644 index 0000000..9c4ad40 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/SingleServerBatchIdentifier.java @@ -0,0 +1,42 @@ +package meerkat.bulletinboard; + +import com.google.protobuf.Int64Value; + +/** + * Created by Arbel Deutsch Peled on 16-Jun-16. + * Single-server implementation of the BatchIdentifier interface + */ +final class SingleServerBatchIdentifier implements AsyncBulletinBoardClient.BatchIdentifier { + + private final Int64Value batchId; + + private int length; + + public SingleServerBatchIdentifier(Int64Value batchId) { + this.batchId = batchId; + length = 0; + } + + public SingleServerBatchIdentifier(long batchId) { + this(Int64Value.newBuilder().setValue(batchId).build()); + } + + public Int64Value getBatchId() { + return batchId; + } + + /** + * Overrides the existing length with the new one only if the new length is longer + * @param newLength + */ + public void setLength(int newLength) { + if (newLength > length) { + length = newLength; + } + } + + public int getLength() { + return length; + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/SingleServerBulletinBoardClient.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/SingleServerBulletinBoardClient.java new file mode 100644 index 0000000..e732059 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/SingleServerBulletinBoardClient.java @@ -0,0 +1,875 @@ +package meerkat.bulletinboard; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListeningScheduledExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.Int64Value; +import com.google.protobuf.Timestamp; +import meerkat.bulletinboard.workers.singleserver.*; +import meerkat.comm.CommunicationException; +import meerkat.crypto.concrete.SHA256Digest; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.protobuf.Crypto; +import meerkat.protobuf.Voting.BulletinBoardClientParams; +import meerkat.util.BulletinBoardUtils; + +import javax.ws.rs.client.Client; +import java.lang.Iterable; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by Arbel Deutsch Peled on 28-Dec-15. + * + * This class implements the asynchronous Bulletin Board Client interface + * It only handles a single Bulletin Board Server + * If the list of servers contains more than one server: the server actually used is the first one + * The class further implements a delayed access to the server after a communication error occurs + */ +public class SingleServerBulletinBoardClient implements SubscriptionBulletinBoardClient { + + protected Client client; + + protected BulletinBoardDigest digest; + + private String dbAddress; + + private final int MAX_RETRIES = 11; + + private final ListeningScheduledExecutorService executorService; + + private long lastServerErrorTime; + + private final long FAIL_DELAY_IN_MILLISECONDS; + + private final long SUBSCRIPTION_INTERVAL_IN_MILLISECONDS; + + /** + * Notify the client that a job has failed + * This makes new scheduled jobs be scheduled for a later time (after the given delay) + */ + protected void fail() { + + // Update last fail time + lastServerErrorTime = System.currentTimeMillis(); + + } + + private class SynchronousRetry { + + private final SingleServerWorker worker; + + private String thrown; + + public SynchronousRetry(SingleServerWorker worker) { + this.worker = worker; + this.thrown = "Could not contact server. Errors follow:\n"; + } + + OUT run() throws CommunicationException { + + do { + + try { + return worker.call(); + } catch (Exception e) { + thrown += e.getCause() + " " + e.getMessage() + "\n"; + } + + try { + Thread.sleep(FAIL_DELAY_IN_MILLISECONDS); + } catch (InterruptedException e) { + //TODO: log + } + + worker.decMaxRetry(); + + } while (worker.isRetry()); + + throw new CommunicationException(thrown); + + } + + } + + /** + * This method adds a worker to the scheduled queue of the threadpool + * If the server is in an accessible state: the job is submitted for immediate handling + * If the server is not accessible: the job is scheduled for a later time + * @param worker is the worker that should be scheduled for work + * @param callback is the class containing callbacks for handling job completion/failure + */ + protected void scheduleWorker(SingleServerWorker worker, FutureCallback callback){ + + long timeSinceLastServerError = System.currentTimeMillis() - lastServerErrorTime; + + if (timeSinceLastServerError >= FAIL_DELAY_IN_MILLISECONDS) { + + // Schedule for immediate processing + Futures.addCallback(executorService.submit(worker), callback); + + } else { + + // Schedule for processing immediately following delay expiry + Futures.addCallback(executorService.schedule( + worker, + FAIL_DELAY_IN_MILLISECONDS - timeSinceLastServerError, + TimeUnit.MILLISECONDS), + callback); + + } + + } + + /** + * Inner class for handling simple operation results and retrying if needed + */ + class RetryCallback implements FutureCallback { + + private final SingleServerWorker worker; + private final FutureCallback futureCallback; + + public RetryCallback(SingleServerWorker worker, FutureCallback futureCallback) { + this.worker = worker; + this.futureCallback = futureCallback; + } + + @Override + public void onSuccess(T result) { + if (futureCallback != null) + futureCallback.onSuccess(result); + } + + @Override + public void onFailure(Throwable t) { + + // Notify client about failure + fail(); + + // Check if another attempt should be made + + worker.decMaxRetry(); + + if (worker.isRetry()) { + // Perform another attempt + scheduleWorker(worker, this); + } else { + // No more retries: notify caller about failure + if (futureCallback != null) + futureCallback.onFailure(t); + } + + } + + } + + /** + * This callback ties together all the per-batch-data callbacks into a single callback + * It reports success back to the user only if all of the batch-data were successfully posted + * If any batch-data fails to post: this callback reports failure + */ + class PostBatchChunkListCallback implements FutureCallback { + + private final FutureCallback callback; + + private AtomicInteger batchDataRemaining; + private AtomicBoolean aggregatedResult; + + public PostBatchChunkListCallback(int batchDataLength, FutureCallback callback) { + + this.callback = callback; + this.batchDataRemaining = new AtomicInteger(batchDataLength); + this.aggregatedResult = new AtomicBoolean(false); + + } + + @Override + public void onSuccess(Boolean result) { + + if (result){ + this.aggregatedResult.set(true); + } + + if (batchDataRemaining.decrementAndGet() == 0){ + if (callback != null) + callback.onSuccess(this.aggregatedResult.get()); + } + } + + @Override + public void onFailure(Throwable t) { + + // Notify caller about failure + if (callback != null) + callback.onFailure(t); + + } + } + + private class ReadBatchCallback implements FutureCallback> { + + private final BulletinBoardMessage stub; + private final FutureCallback callback; + + public ReadBatchCallback(BulletinBoardMessage stub, FutureCallback callback) { + this.stub = stub; + this.callback = callback; + } + + @Override + public void onSuccess(List result) { + callback.onSuccess(BulletinBoardUtils.gatherBatch(stub, result)); + } + + @Override + public void onFailure(Throwable t) { + callback.onFailure(t); + } + + } + + /** + * This callback receives a message which may be a stub + * If the message is not a stub: it returns it as is to a callback function + * If it is a stub: it schedules a read of the batch data which will return a complete message to the callback function + */ + class CompleteMessageReadCallback implements FutureCallback>{ + + private final FutureCallback callback; + + public CompleteMessageReadCallback(FutureCallback callback) { + + this.callback = callback; + + } + + @Override + public void onSuccess(List result) { + if (result.size() <= 0) { + onFailure(new CommunicationException("Could not find required message on the server.")); + } else { + + BulletinBoardMessage msg = result.get(0); + + if (msg.getMsg().getDataTypeCase() != UnsignedBulletinBoardMessage.DataTypeCase.MSGID) { + callback.onSuccess(msg); + } else { + + // Create job with MAX retries for retrieval of the Batch Data List + + BatchQuery batchQuery = BatchQuery.newBuilder() + .setMsgID(MessageID.newBuilder() + .setID(msg.getMsg().getMsgId()) + .build()) + .build(); + + SingleServerReadBatchWorker batchWorker = new SingleServerReadBatchWorker(dbAddress, batchQuery, MAX_RETRIES); + + scheduleWorker(batchWorker, new ReadBatchCallback(msg, callback)); + + } + } + } + + @Override + public void onFailure(Throwable t) { + callback.onFailure(t); + } + + } + + + /** + * Inner class for handling returned values of subscription operations + * This class's methods also ensure continued operation of the subscription + */ + class SubscriptionCallback implements FutureCallback> { + + private SingleServerReadMessagesWorker worker; + private final FutureCallback> callback; + + private MessageFilterList.Builder filterBuilder; + + public SubscriptionCallback(SingleServerReadMessagesWorker worker, FutureCallback> callback) { + this.worker = worker; + this.callback = callback; + filterBuilder = worker.getPayload().toBuilder(); + + } + + @Override + public void onSuccess(List result) { + + // Report new messages to user + if (callback != null) + callback.onSuccess(result); + + // Update filter if needed + + if (result.size() > 0) { + + // Remove last filter from list (MIN_ENTRY one) + filterBuilder.removeFilter(filterBuilder.getFilterCount() - 1); + + // Add updated MIN_ENTRY filter (entry number is successor of last received entry's number) + filterBuilder.addFilter(MessageFilter.newBuilder() + .setType(FilterType.MIN_ENTRY) + .setEntry(result.get(result.size() - 1).getEntryNum() + 1) + .build()); + + } + + // Create new worker with updated task + worker = new SingleServerReadMessagesWorker(worker.serverAddress, filterBuilder.build(), MAX_RETRIES); + + RetryCallback> retryCallback = new RetryCallback<>(worker, this); + + // Schedule the worker to run after the given interval has elapsed + Futures.addCallback(executorService.schedule(worker, SUBSCRIPTION_INTERVAL_IN_MILLISECONDS, TimeUnit.MILLISECONDS), retryCallback); + + } + + @Override + public void onFailure(Throwable t) { + + // Notify client about failure + fail(); + + // Notify caller about failure and terminate subscription + if (callback != null) + callback.onFailure(t); + } + } + + public SingleServerBulletinBoardClient(ListeningScheduledExecutorService executorService, + long failDelayInMilliseconds, + long subscriptionIntervalInMilliseconds) { + + this.executorService = executorService; + + this.FAIL_DELAY_IN_MILLISECONDS = failDelayInMilliseconds; + this.SUBSCRIPTION_INTERVAL_IN_MILLISECONDS = subscriptionIntervalInMilliseconds; + + // Set server error time to a time sufficiently in the past to make new jobs go through + lastServerErrorTime = System.currentTimeMillis() - failDelayInMilliseconds; + + } + + public SingleServerBulletinBoardClient(int threadPoolSize, long failDelayInMilliseconds, long subscriptionIntervalInMilliseconds) { + + this(MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(threadPoolSize)), + failDelayInMilliseconds, + subscriptionIntervalInMilliseconds); + + } + + /** + * Stores database location, initializes the web Client and + * @param clientParams contains the data needed to access the DBs + */ + @Override + public void init(BulletinBoardClientParams clientParams) { + + this.digest = new GenericBulletinBoardDigest(new SHA256Digest()); + + // Remove all but first DB address + this.dbAddress = clientParams.getBulletinBoardAddress(0); + + } + + // Synchronous methods + + @Override + public MessageID postMessage(BulletinBoardMessage msg) throws CommunicationException { + + SingleServerPostMessageWorker worker = new SingleServerPostMessageWorker(dbAddress, msg, MAX_RETRIES); + + SynchronousRetry retry = new SynchronousRetry<>(worker); + + retry.run(); + + digest.reset(); + digest.update(msg); + + return digest.digestAsMessageID(); + + } + + @Override + public float getRedundancy(MessageID id) throws CommunicationException { + + SingleServerGetRedundancyWorker worker = new SingleServerGetRedundancyWorker(dbAddress, id, MAX_RETRIES); + + SynchronousRetry retry = new SynchronousRetry<>(worker); + + return retry.run(); + + } + + @Override + public List readMessages(MessageFilterList filterList) throws CommunicationException { + + SingleServerReadMessagesWorker worker = new SingleServerReadMessagesWorker(dbAddress, filterList, MAX_RETRIES); + + SynchronousRetry> retry = new SynchronousRetry<>(worker); + + return retry.run(); + + } + + @Override + public MessageID postAsBatch(BulletinBoardMessage msg, int chunkSize) throws CommunicationException { + + // Begin the batch and obtain identifier + + BeginBatchMessage beginBatchMessage = BeginBatchMessage.newBuilder() + .addAllTag(msg.getMsg().getTagList()) + .build(); + + SingleServerBeginBatchWorker beginBatchWorker = new SingleServerBeginBatchWorker(dbAddress, beginBatchMessage, MAX_RETRIES); + + SynchronousRetry beginRetry = new SynchronousRetry<>(beginBatchWorker); + + Int64Value identifier = beginRetry.run(); + + // Post data chunks + + List batchChunkList = BulletinBoardUtils.breakToBatch(msg, chunkSize); + + BatchMessage.Builder builder = BatchMessage.newBuilder().setBatchId(identifier.getValue()); + + int position = 0; + + for (BatchChunk data : batchChunkList) { + + builder.setSerialNum(position).setData(data); + + SingleServerPostBatchWorker dataWorker = new SingleServerPostBatchWorker(dbAddress, builder.build(), MAX_RETRIES); + + SynchronousRetry dataRetry = new SynchronousRetry<>(dataWorker); + + dataRetry.run(); + + // Increment position in batch + position++; + + } + + // Close batch + + CloseBatchMessage closeBatchMessage = CloseBatchMessage.newBuilder() + .setBatchId(identifier.getValue()) + .addAllSig(msg.getSigList()) + .setTimestamp(msg.getMsg().getTimestamp()) + .setBatchLength(position) + .build(); + + SingleServerCloseBatchWorker closeBatchWorker = new SingleServerCloseBatchWorker(dbAddress, closeBatchMessage, MAX_RETRIES); + + SynchronousRetry retry = new SynchronousRetry<>(closeBatchWorker); + + retry.run(); + + // Calculate ID and return + + digest.reset(); + digest.update(msg); + + return digest.digestAsMessageID(); + + } + + @Override + public BulletinBoardMessage readMessage(MessageID msgID) throws CommunicationException { + + // Retrieve message (which may be a stub) + + MessageFilterList filterList = MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.MSG_ID) + .setId(msgID.getID()) + .build()) + .build(); + + SingleServerReadMessagesWorker stubWorker = new SingleServerReadMessagesWorker(dbAddress, filterList, MAX_RETRIES); + + SynchronousRetry> retry = new SynchronousRetry<>(stubWorker); + + List messages = retry.run(); + + if (messages.size() <= 0) { + throw new CommunicationException("Could not find message in database."); + } + + BulletinBoardMessage msg = messages.get(0); + + if (msg.getMsg().getDataTypeCase() != UnsignedBulletinBoardMessage.DataTypeCase.MSGID) { + + // We retrieved a complete message. Return it. + return msg; + + } else { + + // We retrieved a stub. Retrieve data. + return readBatchData(msg); + + } + + } + + @Override + public BulletinBoardMessage readBatchData(BulletinBoardMessage stub) throws CommunicationException, IllegalArgumentException { + + BatchQuery batchQuery = BatchQuery.newBuilder() + .setMsgID(MessageID.newBuilder() + .setID(stub.getMsg().getMsgId()) + .build()) + .setStartPosition(0) + .build(); + + SingleServerReadBatchWorker readBatchWorker = new SingleServerReadBatchWorker(dbAddress, batchQuery, MAX_RETRIES); + + SynchronousRetry> batchRetry = new SynchronousRetry<>(readBatchWorker); + + List batchChunkList = batchRetry.run(); + + return BulletinBoardUtils.gatherBatch(stub, batchChunkList); + + } + + @Override + public SyncQuery generateSyncQuery(GenerateSyncQueryParams generateSyncQueryParams) throws CommunicationException { + + SingleServerGenerateSyncQueryWorker worker = + new SingleServerGenerateSyncQueryWorker(dbAddress, generateSyncQueryParams, MAX_RETRIES); + + SynchronousRetry retry = new SynchronousRetry<>(worker); + + return retry.run(); + + } + + // Asynchronous methods + + @Override + public MessageID postMessage(BulletinBoardMessage msg, FutureCallback callback) { + + // Create worker with redundancy 1 and MAX_RETRIES retries + SingleServerPostMessageWorker worker = new SingleServerPostMessageWorker(dbAddress, msg, MAX_RETRIES); + + // Submit worker and create callback + scheduleWorker(worker, new RetryCallback<>(worker, callback)); + + // Calculate the correct message ID and return it + digest.reset(); + digest.update(msg.getMsg()); + return digest.digestAsMessageID(); + + } + + private class PostBatchDataCallback implements FutureCallback { + + private final BulletinBoardMessage msg; + private final BatchIdentifier identifier; + private final FutureCallback callback; + + public PostBatchDataCallback(BulletinBoardMessage msg, BatchIdentifier identifier, FutureCallback callback) { + this.msg = msg; + this.identifier = identifier; + this.callback = callback; + } + + @Override + public void onSuccess(Boolean result) { + closeBatch( + identifier, + msg.getMsg().getTimestamp(), + msg.getSigList(), + callback + ); + } + + @Override + public void onFailure(Throwable t) { + if (callback != null) + callback.onFailure(t); + } + + } + + private class ContinueBatchCallback implements FutureCallback { + + private final BulletinBoardMessage msg; + private final int chunkSize; + private final FutureCallback callback; + + public ContinueBatchCallback(BulletinBoardMessage msg, int chunkSize, FutureCallback callback) { + this.msg = msg; + this.chunkSize = chunkSize; + this.callback = callback; + } + + @Override + public void onSuccess(BatchIdentifier identifier) { + + List batchChunkList = BulletinBoardUtils.breakToBatch(msg, chunkSize); + + postBatchData( + identifier, + batchChunkList, + 0, + new PostBatchDataCallback(msg, identifier, callback)); + } + + @Override + public void onFailure(Throwable t) { + if (callback != null) + callback.onFailure(t); + } + } + + @Override + public MessageID postAsBatch(BulletinBoardMessage msg, int chunkSize, FutureCallback callback) { + + beginBatch( + msg.getMsg().getTagList(), + new ContinueBatchCallback(msg, chunkSize, callback) + ); + + digest.update(msg); + + return digest.digestAsMessageID(); + + } + + private class BeginBatchCallback implements FutureCallback { + + private final FutureCallback callback; + + public BeginBatchCallback(FutureCallback callback) { + this.callback = callback; + } + + @Override + public void onSuccess(Int64Value result) { + callback.onSuccess(new SingleServerBatchIdentifier(result)); + } + + @Override + public void onFailure(Throwable t) { + callback.onFailure(t); + } + } + + @Override + public void beginBatch(Iterable tags, FutureCallback callback) { + + BeginBatchMessage beginBatchMessage = BeginBatchMessage.newBuilder() + .addAllTag(tags) + .build(); + + // Create worker with redundancy 1 and MAX_RETRIES retries + SingleServerBeginBatchWorker worker = + new SingleServerBeginBatchWorker(dbAddress, beginBatchMessage, MAX_RETRIES); + + // Submit worker and create callback + scheduleWorker(worker, new RetryCallback<>(worker, new BeginBatchCallback(callback))); + + } + + + @Override + public void postBatchData(BatchIdentifier batchIdentifier, List batchChunkList, + int startPosition, FutureCallback callback) throws IllegalArgumentException{ + + // Cast identifier to usable form + + if (!(batchIdentifier instanceof SingleServerBatchIdentifier)){ + throw new IllegalArgumentException("Error: batch identifier supplied was not created by this class."); + } + + SingleServerBatchIdentifier identifier = (SingleServerBatchIdentifier) batchIdentifier; + + // Update batch size + + identifier.setLength(startPosition + batchChunkList.size()); + + // Create a unified callback to aggregate successful posts + + PostBatchChunkListCallback listCallback = new PostBatchChunkListCallback(batchChunkList.size(), callback); + + // Iterate through data list + + BatchMessage.Builder builder = BatchMessage.newBuilder() + .setBatchId(identifier.getBatchId().getValue()); + + for (BatchChunk data : batchChunkList) { + builder.setSerialNum(startPosition).setData(data); + + // Create worker with redundancy 1 and MAX_RETRIES retries + SingleServerPostBatchWorker worker = + new SingleServerPostBatchWorker(dbAddress, builder.build(), MAX_RETRIES); + + // Create worker with redundancy 1 and MAX_RETRIES retries + scheduleWorker(worker, new RetryCallback<>(worker, listCallback)); + + // Increment position in batch + startPosition++; + } + + } + + @Override + public void postBatchData(BatchIdentifier batchIdentifier, List batchChunkList, FutureCallback callback) + throws IllegalArgumentException { + + postBatchData(batchIdentifier, batchChunkList, 0, callback); + + } + + @Override + public void closeBatch(BatchIdentifier batchIdentifier, Timestamp timestamp, Iterable signatures, FutureCallback callback) + throws IllegalArgumentException { + + if (!(batchIdentifier instanceof SingleServerBatchIdentifier)){ + throw new IllegalArgumentException("Error: batch identifier supplied was not created by this class."); + } + + SingleServerBatchIdentifier identifier = (SingleServerBatchIdentifier) batchIdentifier; + + CloseBatchMessage closeBatchMessage = CloseBatchMessage.newBuilder() + .setBatchId(identifier.getBatchId().getValue()) + .setBatchLength(identifier.getLength()) + .setTimestamp(timestamp) + .addAllSig(signatures) + .build(); + + // Create worker with redundancy 1 and MAX_RETRIES retries + SingleServerCloseBatchWorker worker = + new SingleServerCloseBatchWorker(dbAddress, closeBatchMessage, MAX_RETRIES); + + // Submit worker and create callback + scheduleWorker(worker, new RetryCallback<>(worker, callback)); + + } + + @Override + public void getRedundancy(MessageID id, FutureCallback callback) { + + // Create worker with no retries + SingleServerGetRedundancyWorker worker = new SingleServerGetRedundancyWorker(dbAddress, id, 1); + + // Submit job and create callback + scheduleWorker(worker, new RetryCallback<>(worker, callback)); + + } + + @Override + public void readMessages(MessageFilterList filterList, FutureCallback> callback) { + + // Create job with no retries + SingleServerReadMessagesWorker worker = new SingleServerReadMessagesWorker(dbAddress, filterList, 1); + + // Submit job and create callback + scheduleWorker(worker, new RetryCallback<>(worker, callback)); + + } + + @Override + public void readMessage(MessageID msgID, FutureCallback callback) { + + // Create job with MAX retries for retrieval of the Bulletin Board Message (which may be a stub) + + MessageFilterList filterList = MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.MSG_ID) + .setId(msgID.getID()) + .build()) + .build(); + + BatchQuery batchQuery = BatchQuery.newBuilder() + .setMsgID(msgID) + .setStartPosition(0) + .build(); + + SingleServerReadMessagesWorker messageWorker = new SingleServerReadMessagesWorker(dbAddress, filterList, MAX_RETRIES); + + // Submit jobs with wrapped callbacks + scheduleWorker(messageWorker, new RetryCallback<>(messageWorker, new CompleteMessageReadCallback(callback))); + + } + + @Override + public void readBatchData(BulletinBoardMessage stub, FutureCallback callback) throws IllegalArgumentException{ + + if (stub.getMsg().getDataTypeCase() != UnsignedBulletinBoardMessage.DataTypeCase.MSGID) { + throw new IllegalArgumentException("Message is not a stub and does not contain the required message ID"); + } + + // Create job with MAX retries for retrieval of the Batch Data List + + BatchQuery batchQuery = BatchQuery.newBuilder() + .setMsgID(MessageID.newBuilder() + .setID(stub.getMsg().getMsgId()) + .build()) + .setStartPosition(0) + .build(); + + SingleServerReadBatchWorker batchWorker = new SingleServerReadBatchWorker(dbAddress, batchQuery, MAX_RETRIES); + + scheduleWorker(batchWorker, new RetryCallback<>(batchWorker, new ReadBatchCallback(stub, callback))); + + } + + @Override + public void querySync(SyncQuery syncQuery, FutureCallback callback) { + + SingleServerQuerySyncWorker worker = new SingleServerQuerySyncWorker(dbAddress, syncQuery, MAX_RETRIES); + + scheduleWorker(worker, new RetryCallback<>(worker, callback)); + + } + + @Override + public void subscribe(MessageFilterList filterList, long startEntry, FutureCallback> callback) { + // Remove all existing MIN_ENTRY filters and create new one that starts at 0 + + MessageFilterList.Builder filterListBuilder = filterList.toBuilder(); + + Iterator iterator = filterListBuilder.getFilterList().iterator(); + while (iterator.hasNext()) { + MessageFilter filter = iterator.next(); + + if (filter.getType() == FilterType.MIN_ENTRY){ + iterator.remove(); + } + } + filterListBuilder.addFilter(MessageFilter.newBuilder() + .setType(FilterType.MIN_ENTRY) + .setEntry(startEntry) + .build()); + + // Create job with no retries + SingleServerReadMessagesWorker worker = new SingleServerReadMessagesWorker(dbAddress, filterListBuilder.build(), MAX_RETRIES); + + // Submit job and create callback that retries on failure and handles repeated subscription + scheduleWorker(worker, new RetryCallback<>(worker, new SubscriptionCallback(worker, callback))); + } + + @Override + public void subscribe(MessageFilterList filterList, FutureCallback> callback) { + subscribe(filterList, 0, callback); + } + + @Override + public void close() { + + executorService.shutdown(); + + } +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/SingleServerWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/SingleServerWorker.java new file mode 100644 index 0000000..ecebc12 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/SingleServerWorker.java @@ -0,0 +1,39 @@ +package meerkat.bulletinboard; + +import meerkat.rest.ProtobufMessageBodyReader; +import meerkat.rest.ProtobufMessageBodyWriter; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import java.util.concurrent.Callable; + +/** + * Created by Arbel Deutsch Peled on 02-Jan-16. + */ +public abstract class SingleServerWorker extends BulletinClientWorker implements Callable{ + + // This resource enabled creation of a single Client per thread. + protected static final ThreadLocal clientLocal = + new ThreadLocal () { + @Override protected Client initialValue() { + Client client; + client = ClientBuilder.newClient(); + client.register(ProtobufMessageBodyReader.class); + client.register(ProtobufMessageBodyWriter.class); + + return client; + } + }; + + protected final String serverAddress; + + public SingleServerWorker(String serverAddress, IN payload, int maxRetry) { + super(payload, maxRetry); + this.serverAddress = serverAddress; + } + + public String getServerAddress() { + return serverAddress; + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/ThreadedBulletinBoardClient.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/ThreadedBulletinBoardClient.java index bb46c32..9dd85b9 100644 --- a/bulletin-board-client/src/main/java/meerkat/bulletinboard/ThreadedBulletinBoardClient.java +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/ThreadedBulletinBoardClient.java @@ -1,42 +1,62 @@ package meerkat.bulletinboard; -import com.google.common.util.concurrent.*; -import com.google.protobuf.ByteString; -import meerkat.bulletinboard.callbacks.GetRedundancyFutureCallback; -import meerkat.bulletinboard.callbacks.PostMessageFutureCallback; -import meerkat.bulletinboard.callbacks.ReadMessagesFutureCallback; -import meerkat.comm.CommunicationException; -import meerkat.crypto.Digest; -import meerkat.crypto.concrete.SHA256Digest; -import meerkat.protobuf.BulletinBoardAPI.*; -import meerkat.protobuf.Voting; +import com.google.common.util.concurrent.FutureCallback; +import com.google.protobuf.Timestamp; +import meerkat.bulletinboard.workers.multiserver.*; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.protobuf.Crypto.Signature; +import meerkat.protobuf.Voting.*; + +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * Created by Arbel Deutsch Peled on 05-Dec-15. - * Thread-based implementation of a Bulletin Board Client. + * Thread-based implementation of a Async Bulletin Board Client. * Features: * 1. Handles tasks concurrently. * 2. Retries submitting */ -public class ThreadedBulletinBoardClient implements BulletinBoardClient { +public class ThreadedBulletinBoardClient extends SimpleBulletinBoardClient implements AsyncBulletinBoardClient { - private final static int THREAD_NUM = 10; - ListeningExecutorService listeningExecutor; + // Executor service for handling jobs + private final static int JOBS_THREAD_NUM = 5; + private ExecutorService executorService; - private Digest digest; + // Per-server clients + private List clients; - private List meerkatDBs; - private String postSubAddress; - private String readSubAddress; + private BulletinBoardDigest batchDigest; + private final static int POST_MESSAGE_RETRY_NUM = 3; private final static int READ_MESSAGES_RETRY_NUM = 1; + private final static int GET_REDUNDANCY_RETRY_NUM = 1; + + private final int SERVER_THREADPOOL_SIZE; + private final long FAIL_DELAY; + private final long SUBSCRIPTION_INTERVAL; + + private static final int DEFAULT_SERVER_THREADPOOL_SIZE = 5; + private static final long DEFAULT_FAIL_DELAY = 5000; + private static final long DEFAULT_SUBSCRIPTION_INTERVAL = 10000; private int minAbsoluteRedundancy; + + public ThreadedBulletinBoardClient(int serverThreadpoolSize, long failDelay, long subscriptionInterval) { + SERVER_THREADPOOL_SIZE = serverThreadpoolSize; + FAIL_DELAY = failDelay; + SUBSCRIPTION_INTERVAL = subscriptionInterval; + } + + public ThreadedBulletinBoardClient() { + this(DEFAULT_SERVER_THREADPOOL_SIZE, DEFAULT_FAIL_DELAY, DEFAULT_SUBSCRIPTION_INTERVAL); + } + /** * Stores database locations and initializes the web Client * Stores the required minimum redundancy. @@ -44,15 +64,29 @@ public class ThreadedBulletinBoardClient implements BulletinBoardClient { * @param clientParams contains the required information */ @Override - public void init(Voting.BulletinBoardClientParams clientParams) { + public void init(BulletinBoardClientParams clientParams) { - meerkatDBs = clientParams.getBulletinBoardAddressList(); + super.init(clientParams); - minAbsoluteRedundancy = (int) (clientParams.getMinRedundancy() * meerkatDBs.size()); + batchDigest = new GenericBulletinBoardDigest(digest); - listeningExecutor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(THREAD_NUM)); + minAbsoluteRedundancy = (int) (clientParams.getMinRedundancy() * (float) clientParams.getBulletinBoardAddressCount()); - digest = new SHA256Digest(); + executorService = Executors.newFixedThreadPool(JOBS_THREAD_NUM); + + clients = new ArrayList<>(clientParams.getBulletinBoardAddressCount()); + for (String address : clientParams.getBulletinBoardAddressList()){ + + SingleServerBulletinBoardClient client = + new SingleServerBulletinBoardClient(SERVER_THREADPOOL_SIZE, FAIL_DELAY, SUBSCRIPTION_INTERVAL); + + client.init(BulletinBoardClientParams.newBuilder() + .addBulletinBoardAddress(address) + .build()); + + clients.add(client); + + } } @@ -61,21 +95,101 @@ public class ThreadedBulletinBoardClient implements BulletinBoardClient { * Retry failed DBs * @param msg is the message, * @return the message ID for later retrieval - * @throws CommunicationException */ @Override - public MessageID postMessage(BulletinBoardMessage msg, ClientCallback callback){ + public MessageID postMessage(BulletinBoardMessage msg, FutureCallback callback){ // Create job - BulletinClientJob job = new BulletinClientJob(meerkatDBs, minAbsoluteRedundancy, BulletinClientJob.JobType.POST_MESSAGE, msg, -1); + MultiServerPostMessageWorker worker = + new MultiServerPostMessageWorker(clients, minAbsoluteRedundancy, msg, POST_MESSAGE_RETRY_NUM, callback); - // Submit job and create callback - Futures.addCallback(listeningExecutor.submit(new BulletinClientWorker(job)), new PostMessageFutureCallback(listeningExecutor, callback)); + // Submit job + executorService.submit(worker); // Calculate the correct message ID and return it - digest.reset(); - digest.update(msg.getMsg()); - return MessageID.newBuilder().setID(ByteString.copyFrom(digest.digest())).build(); + batchDigest.reset(); + batchDigest.update(msg.getMsg()); + return batchDigest.digestAsMessageID(); + + } + + @Override + public MessageID postAsBatch(BulletinBoardMessage msg, int chunkSize, FutureCallback callback) { + + // Create job + MultiServerPostBatchWorker worker = + new MultiServerPostBatchWorker(clients, minAbsoluteRedundancy, msg, chunkSize, POST_MESSAGE_RETRY_NUM, callback); + + // Submit job + executorService.submit(worker); + + // Calculate the correct message ID and return it + batchDigest.reset(); + batchDigest.update(msg); + return batchDigest.digestAsMessageID(); + + } + + @Override + public void beginBatch(Iterable tags, FutureCallback callback) { + + // Create job + MultiServerBeginBatchWorker worker = + new MultiServerBeginBatchWorker(clients, minAbsoluteRedundancy, tags, POST_MESSAGE_RETRY_NUM, callback); + + // Submit job + executorService.submit(worker); + + } + + @Override + public void postBatchData(BatchIdentifier batchIdentifier, List batchChunkList, + int startPosition, FutureCallback callback) throws IllegalArgumentException { + + // Cast identifier to usable form + + if (!(batchIdentifier instanceof MultiServerBatchIdentifier)){ + throw new IllegalArgumentException("Error: batch identifier supplied was not created by this class."); + } + + MultiServerBatchIdentifier identifier = (MultiServerBatchIdentifier) batchIdentifier; + + BatchDataContainer batchDataContainer = new BatchDataContainer(identifier, batchChunkList, startPosition); + + // Create job + MultiServerPostBatchDataWorker worker = + new MultiServerPostBatchDataWorker(clients, minAbsoluteRedundancy, batchDataContainer, POST_MESSAGE_RETRY_NUM, callback); + + // Submit job + executorService.submit(worker); + + } + + @Override + public void postBatchData(BatchIdentifier batchIdentifier, List batchChunkList, FutureCallback callback) + throws IllegalArgumentException { + + postBatchData(batchIdentifier, batchChunkList, 0, callback); + + } + + @Override + public void closeBatch(BatchIdentifier payload, Timestamp timestamp, Iterable signatures, FutureCallback callback) + throws IllegalArgumentException{ + + if (!(payload instanceof MultiServerBatchIdentifier)) { + throw new IllegalArgumentException("Error: batch identifier supplied was not created by this class."); + } + + MultiServerBatchIdentifier identifier = (MultiServerBatchIdentifier) payload; + + // Create job + MultiServerCloseBatchWorker worker = + new MultiServerCloseBatchWorker(clients, minAbsoluteRedundancy, identifier, timestamp, signatures, POST_MESSAGE_RETRY_NUM, callback); + + // Submit job + executorService.submit(worker); + } /** @@ -83,17 +197,16 @@ public class ThreadedBulletinBoardClient implements BulletinBoardClient { * Return the number of databases in which the message was found * Only try once per DB * Ignore communication exceptions in specific databases - * @param id is the requested message ID - * @return the number of DBs in which retrieval was successful */ @Override - public void getRedundancy(MessageID id, ClientCallback callback) { + public void getRedundancy(MessageID id, FutureCallback callback) { // Create job - BulletinClientJob job = new BulletinClientJob(meerkatDBs, minAbsoluteRedundancy, BulletinClientJob.JobType.GET_REDUNDANCY, id, 1); + MultiServerGetRedundancyWorker worker = + new MultiServerGetRedundancyWorker(clients, minAbsoluteRedundancy, id, GET_REDUNDANCY_RETRY_NUM, callback); - // Submit job and create callback - Futures.addCallback(listeningExecutor.submit(new BulletinClientWorker(job)), new GetRedundancyFutureCallback(listeningExecutor, callback)); + // Submit job + executorService.submit(worker); } @@ -101,27 +214,69 @@ public class ThreadedBulletinBoardClient implements BulletinBoardClient { * Go through the DBs and try to retrieve messages according to the specified filter * If at the operation is successful for some DB: return the results and stop iterating * If no operation is successful: return null (NOT blank list) - * @param filterList return only messages that match the filters (null means no filtering). - * @return */ @Override - public void readMessages(MessageFilterList filterList, ClientCallback> callback) { + public void readMessages(MessageFilterList filterList, FutureCallback> callback) { // Create job - BulletinClientJob job = new BulletinClientJob(meerkatDBs, minAbsoluteRedundancy, BulletinClientJob.JobType.READ_MESSAGES, - filterList, READ_MESSAGES_RETRY_NUM); + MultiServerReadMessagesWorker worker = + new MultiServerReadMessagesWorker(clients, minAbsoluteRedundancy, filterList, READ_MESSAGES_RETRY_NUM, callback); - // Submit job and create callback - Futures.addCallback(listeningExecutor.submit(new BulletinClientWorker(job)), new ReadMessagesFutureCallback(listeningExecutor, callback)); + // Submit job + executorService.submit(worker); } + @Override + public void readMessage(MessageID msgID, FutureCallback callback) { + + //Create job + MultiServerReadMessageWorker worker = + new MultiServerReadMessageWorker(clients, minAbsoluteRedundancy, msgID, READ_MESSAGES_RETRY_NUM, callback); + + // Submit job + executorService.submit(worker); + + } + + @Override + public void readBatchData(BulletinBoardMessage stub, FutureCallback callback) throws IllegalArgumentException { + + if (stub.getMsg().getDataTypeCase() != UnsignedBulletinBoardMessage.DataTypeCase.MSGID) { + throw new IllegalArgumentException("Message is not a stub and does not contain the required message ID"); + } + + // Create job + MultiServerReadBatchDataWorker worker = + new MultiServerReadBatchDataWorker(clients, minAbsoluteRedundancy, stub, READ_MESSAGES_RETRY_NUM, callback); + + // Submit job + executorService.submit(worker); + + } + + /** + * This method is not supported by this class! + * This is because it has no meaning when considering more than one server without knowing which server will be contacted + */ + @Override + public void querySync(SyncQuery syncQuery, FutureCallback callback) { + callback.onFailure(new IllegalAccessError("querySync is not supported by this class")); + } + @Override public void close() { + super.close(); + try { - listeningExecutor.shutdown(); - while (! listeningExecutor.isShutdown()) { - listeningExecutor.awaitTermination(10, TimeUnit.SECONDS); + + for (SingleServerBulletinBoardClient client : clients){ + client.close(); + } + + executorService.shutdown(); + while (! executorService.isShutdown()) { + executorService.awaitTermination(10, TimeUnit.SECONDS); } } catch (InterruptedException e) { System.err.println(e.getCause() + " " + e.getMessage()); diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/ThreadedBulletinBoardSubscriber.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/ThreadedBulletinBoardSubscriber.java new file mode 100644 index 0000000..252d6e3 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/ThreadedBulletinBoardSubscriber.java @@ -0,0 +1,276 @@ +package meerkat.bulletinboard; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.protobuf.Timestamp; +import meerkat.comm.CommunicationException; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.util.BulletinBoardUtils; + +import static meerkat.protobuf.BulletinBoardAPI.FilterType.*; + +import java.util.*; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Created by Arbel Deutsch Peled on 03-Mar-16. + * A multi-server implementation of the {@link BulletinBoardSubscriber} + */ +public class ThreadedBulletinBoardSubscriber implements BulletinBoardSubscriber { + + protected final Collection clients; + protected final BulletinBoardClient localClient; + + protected Iterator clientIterator; + protected SubscriptionBulletinBoardClient currentClient; + + private long lastServerSwitchTime; + + private AtomicBoolean isSyncInProgress; + private Semaphore rescheduleSemaphore; + + private AtomicBoolean stopped; + + private static final Float[] BREAKPOINTS = {0.5f, 0.75f, 0.9f, 0.95f, 0.99f, 0.999f}; + + public ThreadedBulletinBoardSubscriber(Collection clients, BulletinBoardClient localClient) { + + this.clients = clients; + this.localClient = localClient; + + lastServerSwitchTime = System.currentTimeMillis(); + + clientIterator = clients.iterator(); + currentClient = clientIterator.next(); + + isSyncInProgress = new AtomicBoolean(false); + rescheduleSemaphore = new Semaphore(1); + + stopped = new AtomicBoolean(false); + + } + + /** + * Moves to next client and performs resync with it + */ + private void nextClient() { + + try { + + rescheduleSemaphore.acquire(); + + if (!clientIterator.hasNext()){ + clientIterator = clients.iterator(); + } + + currentClient = clientIterator.next(); + + lastServerSwitchTime = System.currentTimeMillis(); + + isSyncInProgress.set(false); + + rescheduleSemaphore.release(); + + } catch (InterruptedException e) { + // TODO: log + // Do not change client + } + + } + + private abstract class SubscriberCallback implements FutureCallback { + + protected final MessageFilterList filterList; + protected final FutureCallback> callback; + private final long invocationTime; + + public SubscriberCallback(MessageFilterList filterList, FutureCallback> callback) { + + this.filterList = filterList; + this.callback = callback; + this.invocationTime = System.currentTimeMillis(); + + } + + /** + * Handles resyncing process for the given subscription after a server is switched + * Specifically: generates a sync query from the local database and uses it to query the current server + */ + private void reSync() { + + SyncQuery syncQuery = null; + try { + + syncQuery = localClient.generateSyncQuery(GenerateSyncQueryParams.newBuilder() + .setFilterList(filterList) + .addAllBreakpointList(Arrays.asList(BREAKPOINTS)) + .build()); + + } catch (CommunicationException e) { + + // Handle failure in standard way + onFailure(e); + + } + + currentClient.querySync(syncQuery, new SyncQueryCallback(filterList, callback)); + + } + + /** + * Reschedules the subscription + */ + private void reschedule() { + + try { + + rescheduleSemaphore.acquire(); + + reSync(); + + rescheduleSemaphore.release(); + + + } catch (InterruptedException e) { + + //TODO: log + + if (callback != null) + callback.onFailure(e); // Hard error: Cannot guarantee subscription safety + + } + + } + + @Override + public void onFailure(Throwable t) { + + // If server failure is not already known: switch to next client and resync + if (invocationTime > lastServerSwitchTime){ + + // Make sure only what thread switches the client + if (isSyncInProgress.compareAndSet(false, true)){ + nextClient(); + } + + } + + reschedule(); + + } + + } + + /** + * Provides handling logic for resync query callback operation + * Receives a SyncQueryResponse and reads the missing data (starting from the received timestamp) if needed + */ + protected class SyncQueryCallback extends SubscriberCallback { + + public SyncQueryCallback (MessageFilterList filterList, FutureCallback> callback) { + + super(filterList, callback); + + } + + @Override + public void onSuccess(SyncQueryResponse result) { + + final Timestamp DEFAULT_TIME = BulletinBoardUtils.toTimestampProto(946728000); // Year 2000 + + // Read required messages according to received Timestamp + + Timestamp syncTimestamp; + + if (result.hasLastTimeOfSync()) { + syncTimestamp = result.getLastTimeOfSync(); // Use returned time of sync + } else { + syncTimestamp = DEFAULT_TIME; // Get all messages + } + + MessageFilterList timestampedFilterList = filterList.toBuilder() + .removeFilter(filterList.getFilterCount()-1) // Remove MIN_ENTRY filter + .addFilter(MessageFilter.newBuilder() // Add timestamp filter + .setType(AFTER_TIME) + .setTimestamp(syncTimestamp) + .build()) + .build(); + + currentClient.readMessages(timestampedFilterList, new ReSyncCallback(filterList, callback, result.getLastEntryNum())); + + } + + } + + /** + * Provides handling logic for callback of resyncing process + * Receives the missing messages, handles them and resubscribes + */ + protected class ReSyncCallback extends SubscriberCallback> { + + private long minEntry; + + public ReSyncCallback (MessageFilterList filterList, FutureCallback> callback, long minEntry) { + + super(filterList, callback); + + this.minEntry = minEntry; + + } + + @Override + public void onSuccess(List result) { + + // Propagate result to caller + if (callback != null) + callback.onSuccess(result); + + // Renew subscription + + MessageFilterList newFilterList = filterList.toBuilder() + .removeFilter(filterList.getFilterCount()-1) // Remove current MIN_ENTRY filter + .addFilter(MessageFilter.newBuilder() // Add new MIN_ENTRY filter for current server + .setType(MIN_ENTRY) + .setEntry(minEntry) + .build()) + .build(); + + currentClient.subscribe(newFilterList, callback); + + } + + } + + /** + * Provides the handling logic for results and failures of main subscription (while there are no errors) + */ + protected class SubscriptionCallback extends SubscriberCallback> { + + public SubscriptionCallback(MessageFilterList filterList, FutureCallback> callback){ + super(filterList, callback); + } + + @Override + public void onSuccess(List result) { + + // Propagate result to caller + if (callback != null) + callback.onSuccess(result); + + } + + } + + @Override + public void subscribe(MessageFilterList filterList, long startEntry, FutureCallback> callback) { + + currentClient.subscribe(filterList, startEntry, new SubscriptionCallback(filterList, callback)); + + } + + @Override + public void subscribe(MessageFilterList filterList, FutureCallback> callback) { + subscribe(filterList, 0, callback); + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/ClientFutureCallback.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/ClientFutureCallback.java deleted file mode 100644 index 7c5b7b0..0000000 --- a/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/ClientFutureCallback.java +++ /dev/null @@ -1,25 +0,0 @@ -package meerkat.bulletinboard.callbacks; - -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListeningExecutorService; -import meerkat.bulletinboard.BulletinClientJob; -import meerkat.bulletinboard.BulletinClientJobResult; -import meerkat.bulletinboard.BulletinClientWorker; -import meerkat.protobuf.BulletinBoardAPI; - -import java.util.List; - -/** - * This is a future callback used to listen to workers and run on job finish - * Depending on the type of job and the finishing status of the worker: a decision is made whether to retry or return an error - */ -public abstract class ClientFutureCallback implements FutureCallback { - - protected ListeningExecutorService listeningExecutor; - - ClientFutureCallback(ListeningExecutorService listeningExecutor) { - this.listeningExecutor = listeningExecutor; - } - -} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/GetRedundancyFutureCallback.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/GetRedundancyFutureCallback.java deleted file mode 100644 index 518ed77..0000000 --- a/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/GetRedundancyFutureCallback.java +++ /dev/null @@ -1,38 +0,0 @@ -package meerkat.bulletinboard.callbacks; - -import com.google.common.util.concurrent.ListeningExecutorService; -import meerkat.bulletinboard.BulletinBoardClient; -import meerkat.bulletinboard.BulletinClientJobResult; -import meerkat.protobuf.BulletinBoardAPI.*; - -import java.util.List; - -/** - * This is a future callback used to listen to workers and run on job finish - * Depending on the type of job and the finishing status of the worker: a decision is made whether to retry or return an error - */ -public class GetRedundancyFutureCallback extends ClientFutureCallback { - - private BulletinBoardClient.ClientCallback callback; - - public GetRedundancyFutureCallback(ListeningExecutorService listeningExecutor, - BulletinBoardClient.ClientCallback callback) { - super(listeningExecutor); - this.callback = callback; - } - - @Override - public void onSuccess(BulletinClientJobResult result) { - - int absoluteRedundancy = ((IntMsg) result.getResult()).getValue(); - int totalServers = result.getJob().getServerAddresses().size(); - - callback.handleCallback( ((float) absoluteRedundancy) / ((float) totalServers) ); - - } - - @Override - public void onFailure(Throwable t) { - callback.handleFailure(t); - } -} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/PostMessageFutureCallback.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/PostMessageFutureCallback.java deleted file mode 100644 index 221ae1a..0000000 --- a/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/PostMessageFutureCallback.java +++ /dev/null @@ -1,46 +0,0 @@ -package meerkat.bulletinboard.callbacks; - -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListeningExecutorService; -import meerkat.bulletinboard.BulletinBoardClient; -import meerkat.bulletinboard.BulletinClientJob; -import meerkat.bulletinboard.BulletinClientJobResult; -import meerkat.bulletinboard.BulletinClientWorker; -import meerkat.protobuf.BulletinBoardAPI; - -import java.util.List; - -/** - * This is a future callback used to listen to workers and run on job finish - * Depending on the type of job and the finishing status of the worker: a decision is made whether to retry or return an error - */ -public class PostMessageFutureCallback extends ClientFutureCallback { - - private BulletinBoardClient.ClientCallback callback; - - public PostMessageFutureCallback(ListeningExecutorService listeningExecutor, - BulletinBoardClient.ClientCallback callback) { - super(listeningExecutor); - this.callback = callback; - } - - @Override - public void onSuccess(BulletinClientJobResult result) { - - BulletinClientJob job = result.getJob(); - - job.decMaxRetry(); - - // If redundancy is below threshold: retry - if (job.getMinServers() > 0 && job.isRetry()) { - Futures.addCallback(listeningExecutor.submit(new BulletinClientWorker(job)), this); - } - - callback.handleCallback(null); - } - - @Override - public void onFailure(Throwable t) { - callback.handleFailure(t); - } -} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/ReadMessagesFutureCallback.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/ReadMessagesFutureCallback.java deleted file mode 100644 index 4c43ba2..0000000 --- a/bulletin-board-client/src/main/java/meerkat/bulletinboard/callbacks/ReadMessagesFutureCallback.java +++ /dev/null @@ -1,38 +0,0 @@ -package meerkat.bulletinboard.callbacks; - -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListeningExecutorService; -import meerkat.bulletinboard.BulletinBoardClient; -import meerkat.bulletinboard.BulletinClientJob; -import meerkat.bulletinboard.BulletinClientJobResult; -import meerkat.bulletinboard.BulletinClientWorker; -import meerkat.protobuf.BulletinBoardAPI; - -import java.util.List; - -/** - * This is a future callback used to listen to workers and run on job finish - * Depending on the type of job and the finishing status of the worker: a decision is made whether to retry or return an error - */ -public class ReadMessagesFutureCallback extends ClientFutureCallback { - - private BulletinBoardClient.ClientCallback> callback; - - public ReadMessagesFutureCallback(ListeningExecutorService listeningExecutor, - BulletinBoardClient.ClientCallback> callback) { - super(listeningExecutor); - this.callback = callback; - } - - @Override - public void onSuccess(BulletinClientJobResult result) { - - callback.handleCallback(((BulletinBoardAPI.BulletinBoardMessageList) result.getResult()).getMessageList()); - - } - - @Override - public void onFailure(Throwable t) { - callback.handleFailure(t); - } -} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerBeginBatchWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerBeginBatchWorker.java new file mode 100644 index 0000000..7d64946 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerBeginBatchWorker.java @@ -0,0 +1,96 @@ +package meerkat.bulletinboard.workers.multiserver; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.bulletinboard.MultiServerBatchIdentifier; +import meerkat.bulletinboard.MultiServerWorker; +import meerkat.bulletinboard.SingleServerBulletinBoardClient; +import meerkat.bulletinboard.AsyncBulletinBoardClient.BatchIdentifier; +import meerkat.comm.CommunicationException; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + */ +public class MultiServerBeginBatchWorker extends MultiServerWorker, BatchIdentifier> { + + private BatchIdentifier[] identifiers; + private AtomicInteger remainingServers; + + public MultiServerBeginBatchWorker(List clients, + int minServers, Iterable payload, int maxRetry, + FutureCallback futureCallback) { + + super(clients, minServers, payload, maxRetry, futureCallback); + + identifiers = new BatchIdentifier[clients.size()]; + + for (int i = 0 ; i < identifiers.length ; i++) { + identifiers[i] = null; + } + + remainingServers = new AtomicInteger(clients.size()); + + } + + private class BeginBatchCallback implements FutureCallback { + + private final int clientNum; + + public BeginBatchCallback(int clientNum) { + this.clientNum = clientNum; + } + + private void finishPost() { + + if (remainingServers.decrementAndGet() <= 0){ + + if (minServers.decrementAndGet() <= 0) { + MultiServerBeginBatchWorker.this.onSuccess(new MultiServerBatchIdentifier(identifiers)); + } else { + MultiServerBeginBatchWorker.this.onFailure(new CommunicationException("Could not open batch in enough servers")); + } + } + + } + + @Override + public void onSuccess(BatchIdentifier result) { + + identifiers[clientNum] = result; + finishPost(); + + } + + @Override + public void onFailure(Throwable t) { + finishPost(); + } + } + + @Override + public void onSuccess(BatchIdentifier result) { + succeed(result); + } + + @Override + public void onFailure(Throwable t) { + fail(t); + } + + @Override + public void run() { + + int clientNum = 0; + + for (SingleServerBulletinBoardClient client : clients){ + + client.beginBatch(payload, new BeginBatchCallback(clientNum)); + + clientNum++; + + } + + } +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerCloseBatchWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerCloseBatchWorker.java new file mode 100644 index 0000000..1e5a910 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerCloseBatchWorker.java @@ -0,0 +1,84 @@ +package meerkat.bulletinboard.workers.multiserver; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.protobuf.Timestamp; +import meerkat.bulletinboard.AsyncBulletinBoardClient.BatchIdentifier; +import meerkat.bulletinboard.BatchDataContainer; +import meerkat.bulletinboard.MultiServerBatchIdentifier; +import meerkat.bulletinboard.MultiServerWorker; +import meerkat.bulletinboard.SingleServerBulletinBoardClient; +import meerkat.crypto.DigitalSignature; +import meerkat.protobuf.Crypto; +import meerkat.protobuf.Crypto.Signature; + +import java.util.Iterator; +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + */ +public class MultiServerCloseBatchWorker extends MultiServerWorker { + + private final Timestamp timestamp; + private final Iterable signatures; + + public MultiServerCloseBatchWorker(List clients, + int minServers, MultiServerBatchIdentifier payload, Timestamp timestamp, Iterable signatures, + int maxRetry, FutureCallback futureCallback) { + + super(clients, minServers, payload, maxRetry, futureCallback); + + this.timestamp = timestamp; + this.signatures = signatures; + + } + + @Override + public void run() { + + Iterator identifierIterator = payload.getIdentifiers().iterator(); + + // Iterate through client + + for (SingleServerBulletinBoardClient client : clients) { + + if (identifierIterator.hasNext()) { + + // Fetch the batch identifier supplied by the specific client (may be null if batch open failed on client + + BatchIdentifier identifier = identifierIterator.next(); + + if (identifier != null) { + + // Post the data with the matching identifier to the client + client.closeBatch(identifier, timestamp, signatures, this); + + } else { + + // Count servers with no batch identifier as failed + maxFailedServers.decrementAndGet(); + + } + + } + + } + + } + + @Override + public void onSuccess(Boolean result) { + if (minServers.decrementAndGet() <= 0){ + succeed(result); + } + } + + @Override + public void onFailure(Throwable t) { + if (maxFailedServers.decrementAndGet() <= 0){ + fail(t); + } + } + + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerGenericPostWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerGenericPostWorker.java new file mode 100644 index 0000000..a720eda --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerGenericPostWorker.java @@ -0,0 +1,62 @@ +package meerkat.bulletinboard.workers.multiserver; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.FutureCallback; +import meerkat.bulletinboard.MultiServerWorker; +import meerkat.bulletinboard.SingleServerBulletinBoardClient; +import meerkat.comm.CommunicationException; + +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; +import java.util.Iterator; +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + */ +public abstract class MultiServerGenericPostWorker extends MultiServerWorker { + + public MultiServerGenericPostWorker(List clients, + int minServers, T payload, int maxRetry, + FutureCallback futureCallback) { + + super(clients, minServers, payload, maxRetry, futureCallback); + + } + + protected abstract void doPost(SingleServerBulletinBoardClient client, T payload); + + /** + * This method carries out the actual communication with the servers via HTTP Post + * It accesses the servers one by one and tries to post the payload to each in turn + * The method will only iterate once through the server list + * Successful post to a server results in removing the server from the list + */ + public void run() { + + // Iterate through servers + for (SingleServerBulletinBoardClient client : clients) { + + // Send request to Server + doPost(client, payload); + + } + + } + + @Override + public void onSuccess(Boolean result) { + if (result){ + if (minServers.decrementAndGet() <= 0){ + succeed(Boolean.TRUE); + } + } + } + + @Override + public void onFailure(Throwable t) { + if (maxFailedServers.decrementAndGet() < 0){ + fail(t); + } + } +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerGenericReadWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerGenericReadWorker.java new file mode 100644 index 0000000..f17708b --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerGenericReadWorker.java @@ -0,0 +1,69 @@ +package meerkat.bulletinboard.workers.multiserver; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.bulletinboard.MultiServerWorker; +import meerkat.bulletinboard.SingleServerBulletinBoardClient; +import meerkat.comm.CommunicationException; + +import java.util.Iterator; +import java.util.List; + + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + */ +public abstract class MultiServerGenericReadWorker extends MultiServerWorker{ + + private Iterator clientIterator; + + private String errorString; + + public MultiServerGenericReadWorker(List clients, + int minServers, IN payload, int maxRetry, + FutureCallback futureCallback) { + + super(clients, true, minServers, payload, maxRetry, futureCallback); // Shuffle clients on creation to balance load + + clientIterator = clients.iterator(); + errorString = ""; + + } + + protected abstract void doRead(IN payload, SingleServerBulletinBoardClient client); + + /** + * This method carries out the actual communication with the servers via HTTP Post + * It accesses the servers in a random order until one answers it + * Successful retrieval from any server terminates the method and returns the received values; The list is not changed + */ + public void run(){ + + // Iterate through servers + + if (clientIterator.hasNext()) { + + // Get next server + SingleServerBulletinBoardClient client = clientIterator.next(); + + // Retrieve answer from server + doRead(payload, client); + + } else { + fail(new CommunicationException("Could not contact any server. Errors follow:\n" + errorString)); + } + + } + + @Override + public void onSuccess(OUT msg) { + succeed(msg); + } + + @Override + public void onFailure(Throwable t) { + //TODO: log + errorString += t.getCause() + " " + t.getMessage() + "\n"; + run(); // Retry with next server + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerGetRedundancyWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerGetRedundancyWorker.java new file mode 100644 index 0000000..b76a94f --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerGetRedundancyWorker.java @@ -0,0 +1,67 @@ +package meerkat.bulletinboard.workers.multiserver; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.bulletinboard.MultiServerWorker; +import meerkat.bulletinboard.SingleServerBulletinBoardClient; +import meerkat.comm.CommunicationException; +import meerkat.protobuf.BulletinBoardAPI.*; + +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + */ +public class MultiServerGetRedundancyWorker extends MultiServerWorker { + + private AtomicInteger serversContainingMessage; + private AtomicInteger totalContactedServers; + + public MultiServerGetRedundancyWorker(List clients, + int minServers, MessageID payload, int maxRetry, + FutureCallback futureCallback) { + + super(clients, minServers, payload, maxRetry, futureCallback); // Shuffle clients on creation to balance load + + serversContainingMessage = new AtomicInteger(0); + totalContactedServers = new AtomicInteger(0); + + } + + /** + * This method carries out the actual communication with the servers via HTTP Post + * It accesses the servers in a random order until one answers it + * Successful retrieval from any server terminates the method and returns the received values; The list is not changed + */ + public void run(){ + + // Iterate through clients + for (SingleServerBulletinBoardClient client : clients) { + + // Send request to client + client.getRedundancy(payload,this); + + } + + } + + @Override + public void onSuccess(Float result) { + + if (result > 0.5) { + serversContainingMessage.incrementAndGet(); + } + + if (totalContactedServers.incrementAndGet() >= getClientNumber()){ + succeed(((float) serversContainingMessage.get()) / ((float) getClientNumber())); + } + + } + + @Override + public void onFailure(Throwable t) { + onSuccess(0.0f); + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerPostBatchDataWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerPostBatchDataWorker.java new file mode 100644 index 0000000..8ef5931 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerPostBatchDataWorker.java @@ -0,0 +1,72 @@ +package meerkat.bulletinboard.workers.multiserver; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.bulletinboard.AsyncBulletinBoardClient.BatchIdentifier; +import meerkat.bulletinboard.MultiServerWorker; +import meerkat.bulletinboard.SingleServerBulletinBoardClient; +import meerkat.bulletinboard.BatchDataContainer; + +import java.util.Iterator; +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + */ +public class MultiServerPostBatchDataWorker extends MultiServerWorker { + + public MultiServerPostBatchDataWorker(List clients, + int minServers, BatchDataContainer payload, int maxRetry, + FutureCallback futureCallback) { + + super(clients, minServers, payload, maxRetry, futureCallback); + + } + + @Override + public void run() { + + Iterator identifierIterator = payload.batchId.getIdentifiers().iterator(); + + // Iterate through client + + for (SingleServerBulletinBoardClient client : clients) { + + if (identifierIterator.hasNext()) { + + // Fetch the batch identifier supplied by the specific client (may be null if batch open failed on client + + BatchIdentifier identifier = identifierIterator.next(); + + if (identifier != null) { + + // Post the data with the matching identifier to the client + client.postBatchData(identifier, payload.batchChunkList, payload.startPosition, this); + + } else { + + // Count servers with no batch identifier as failed + maxFailedServers.decrementAndGet(); + + } + + } + + } + + } + + @Override + public void onSuccess(Boolean result) { + if (minServers.decrementAndGet() <= 0){ + succeed(result); + } + } + + @Override + public void onFailure(Throwable t) { + if (maxFailedServers.decrementAndGet() <= 0){ + fail(t); + } + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerPostBatchWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerPostBatchWorker.java new file mode 100644 index 0000000..b938f52 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerPostBatchWorker.java @@ -0,0 +1,34 @@ +package meerkat.bulletinboard.workers.multiserver; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.bulletinboard.SingleServerBulletinBoardClient; +import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage; + +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + */ +public class MultiServerPostBatchWorker extends MultiServerGenericPostWorker { + + private final int chunkSize; + + public MultiServerPostBatchWorker(List clients, + int minServers, BulletinBoardMessage payload, int chunkSize, int maxRetry, + FutureCallback futureCallback) { + + super(clients, minServers, payload, maxRetry, futureCallback); + + this.chunkSize = chunkSize; + + } + + @Override + protected void doPost(SingleServerBulletinBoardClient client, BulletinBoardMessage payload) { + + client.postAsBatch(payload, chunkSize, this); + + } + + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerPostMessageWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerPostMessageWorker.java new file mode 100644 index 0000000..6d3d702 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerPostMessageWorker.java @@ -0,0 +1,28 @@ +package meerkat.bulletinboard.workers.multiserver; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.bulletinboard.SingleServerBulletinBoardClient; +import meerkat.protobuf.BulletinBoardAPI.*; + +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + */ +public class MultiServerPostMessageWorker extends MultiServerGenericPostWorker { + + public MultiServerPostMessageWorker(List clients, + int minServers, BulletinBoardMessage payload, int maxRetry, + FutureCallback futureCallback) { + + super(clients, minServers, payload, maxRetry, futureCallback); + + } + + @Override + protected void doPost(SingleServerBulletinBoardClient client, BulletinBoardMessage payload) { + client.postMessage(payload, this); + } + + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerReadBatchDataWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerReadBatchDataWorker.java new file mode 100644 index 0000000..769702a --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerReadBatchDataWorker.java @@ -0,0 +1,29 @@ +package meerkat.bulletinboard.workers.multiserver; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.bulletinboard.SingleServerBulletinBoardClient; +import meerkat.protobuf.BulletinBoardAPI.*; + +import java.util.List; + + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + */ +public class MultiServerReadBatchDataWorker extends MultiServerGenericReadWorker { + + public MultiServerReadBatchDataWorker(List clients, + int minServers, BulletinBoardMessage payload, int maxRetry, + FutureCallback futureCallback) { + + super(clients, minServers, payload, maxRetry, futureCallback); + + } + + @Override + protected void doRead(BulletinBoardMessage payload, SingleServerBulletinBoardClient client) { + client.readBatchData(payload, this); + } + + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerReadMessageWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerReadMessageWorker.java new file mode 100644 index 0000000..f84d67e --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerReadMessageWorker.java @@ -0,0 +1,30 @@ +package meerkat.bulletinboard.workers.multiserver; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.bulletinboard.SingleServerBulletinBoardClient; +import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage; +import meerkat.protobuf.BulletinBoardAPI.MessageID; + +import java.util.List; + + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + */ +public class MultiServerReadMessageWorker extends MultiServerGenericReadWorker { + + public MultiServerReadMessageWorker(List clients, + int minServers, MessageID payload, int maxRetry, + FutureCallback futureCallback) { + + super(clients, minServers, payload, maxRetry, futureCallback); + + } + + @Override + protected void doRead(MessageID payload, SingleServerBulletinBoardClient client) { + client.readMessage(payload, this); + } + + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerReadMessagesWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerReadMessagesWorker.java new file mode 100644 index 0000000..980d869 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/multiserver/MultiServerReadMessagesWorker.java @@ -0,0 +1,29 @@ +package meerkat.bulletinboard.workers.multiserver; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.bulletinboard.SingleServerBulletinBoardClient; +import meerkat.protobuf.BulletinBoardAPI.*; + +import java.util.List; + + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + */ +public class MultiServerReadMessagesWorker extends MultiServerGenericReadWorker>{ + + public MultiServerReadMessagesWorker(List clients, + int minServers, MessageFilterList payload, int maxRetry, + FutureCallback> futureCallback) { + + super(clients, minServers, payload, maxRetry, futureCallback); + + } + + @Override + protected void doRead(MessageFilterList payload, SingleServerBulletinBoardClient client) { + client.readMessages(payload, this); + } + + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerBeginBatchWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerBeginBatchWorker.java new file mode 100644 index 0000000..e08b360 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerBeginBatchWorker.java @@ -0,0 +1,53 @@ +package meerkat.bulletinboard.workers.singleserver; + +import com.google.protobuf.Int64Value; +import meerkat.bulletinboard.SingleServerWorker; +import meerkat.comm.CommunicationException; +import meerkat.protobuf.BulletinBoardAPI.BeginBatchMessage; +import meerkat.rest.Constants; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import static meerkat.bulletinboard.BulletinBoardConstants.BEGIN_BATCH_PATH; +import static meerkat.bulletinboard.BulletinBoardConstants.BULLETIN_BOARD_SERVER_PATH; + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + * Tries to contact server once and perform a post operation + */ +public class SingleServerBeginBatchWorker extends SingleServerWorker { + + public SingleServerBeginBatchWorker(String serverAddress, BeginBatchMessage payload, int maxRetry) { + super(serverAddress, payload, maxRetry); + } + + @Override + public Int64Value call() throws Exception { + Client client = clientLocal.get(); + + WebTarget webTarget = client.target(serverAddress).path(BULLETIN_BOARD_SERVER_PATH).path(BEGIN_BATCH_PATH); + Response response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post( + Entity.entity(payload, Constants.MEDIATYPE_PROTOBUF)); + + try { + + Int64Value result = response.readEntity(Int64Value.class); + return result; + + } catch (ProcessingException | IllegalStateException e) { + + // Post to this server failed + throw new CommunicationException("Could not contact the server. Original error: " + e.getMessage()); + + } catch (Exception e) { + throw new CommunicationException("Could not contact the server. Original error: " + e.getMessage()); + } + finally { + response.close(); + } + } +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerCloseBatchWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerCloseBatchWorker.java new file mode 100644 index 0000000..83e27c2 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerCloseBatchWorker.java @@ -0,0 +1,17 @@ +package meerkat.bulletinboard.workers.singleserver; + +import meerkat.protobuf.BulletinBoardAPI.CloseBatchMessage; + +import static meerkat.bulletinboard.BulletinBoardConstants.CLOSE_BATCH_PATH; + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + * Tries to contact server once and perform a stop batch operation + */ +public class SingleServerCloseBatchWorker extends SingleServerGenericPostWorker { + + public SingleServerCloseBatchWorker(String serverAddress, CloseBatchMessage payload, int maxRetry) { + super(serverAddress, CLOSE_BATCH_PATH, payload, maxRetry); + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerGenerateSyncQueryWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerGenerateSyncQueryWorker.java new file mode 100644 index 0000000..71d90e0 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerGenerateSyncQueryWorker.java @@ -0,0 +1,55 @@ +package meerkat.bulletinboard.workers.singleserver; + +import com.google.protobuf.Int64Value; +import meerkat.bulletinboard.SingleServerWorker; +import meerkat.comm.CommunicationException; +import meerkat.protobuf.BulletinBoardAPI.SyncQuery; +import meerkat.protobuf.BulletinBoardAPI.GenerateSyncQueryParams; +import meerkat.rest.Constants; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import static meerkat.bulletinboard.BulletinBoardConstants.BULLETIN_BOARD_SERVER_PATH; +import static meerkat.bulletinboard.BulletinBoardConstants.GENERATE_SYNC_QUERY_PATH; + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + * Tries to contact server once and perform a Sync Query Generation operation + */ +public class SingleServerGenerateSyncQueryWorker extends SingleServerWorker { + + public SingleServerGenerateSyncQueryWorker(String serverAddress, GenerateSyncQueryParams payload, int maxRetry) { + super(serverAddress, payload, maxRetry); + } + + @Override + public SyncQuery call() throws Exception { + + Client client = clientLocal.get(); + + WebTarget webTarget = client.target(serverAddress).path(BULLETIN_BOARD_SERVER_PATH).path(GENERATE_SYNC_QUERY_PATH); + + Response response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(payload, Constants.MEDIATYPE_PROTOBUF)); + + try { + + SyncQuery result = response.readEntity(SyncQuery.class); + return result; + + } catch (ProcessingException | IllegalStateException e) { + + // Post to this server failed + throw new CommunicationException("Could not contact the server. Original error: " + e.getMessage()); + + } catch (Exception e) { + throw new CommunicationException("Could not contact the server. Original error: " + e.getMessage()); + } + finally { + response.close(); + } + } +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerGenericPostWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerGenericPostWorker.java new file mode 100644 index 0000000..08a66d1 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerGenericPostWorker.java @@ -0,0 +1,63 @@ +package meerkat.bulletinboard.workers.singleserver; + +import com.google.protobuf.BoolValue; +import meerkat.bulletinboard.SingleServerWorker; +import meerkat.comm.CommunicationException; +import meerkat.protobuf.Comm.*; +import meerkat.rest.Constants; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import static meerkat.bulletinboard.BulletinBoardConstants.BULLETIN_BOARD_SERVER_PATH; + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + * Tries to contact server once and perform a post operation + */ +public class SingleServerGenericPostWorker extends SingleServerWorker { + + private final String subPath; + + public SingleServerGenericPostWorker(String serverAddress, String subPath, T payload, int maxRetry) { + super(serverAddress, payload, maxRetry); + this.subPath = subPath; + } + + /** + * This method carries out the actual communication with the server via HTTP Post + * It accesses the server and tries to post the payload to it + * Successful post to a server results + * @return TRUE if the operation is successful + * @throws CommunicationException if the operation is unsuccessful + */ + public Boolean call() throws CommunicationException{ + + Client client = clientLocal.get(); + + WebTarget webTarget = client.target(serverAddress).path(BULLETIN_BOARD_SERVER_PATH).path(subPath); + Response response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post( + Entity.entity(payload, Constants.MEDIATYPE_PROTOBUF)); + + try { + + // If a BoolValue entity is returned: the post was successful + response.readEntity(BoolValue.class); + return Boolean.TRUE; + + } catch (ProcessingException | IllegalStateException e) { + + // Post to this server failed + throw new CommunicationException("Could not contact the server"); + + } + finally { + response.close(); + } + + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerGetRedundancyWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerGetRedundancyWorker.java new file mode 100644 index 0000000..23c07af --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerGetRedundancyWorker.java @@ -0,0 +1,85 @@ +package meerkat.bulletinboard.workers.singleserver; + +import meerkat.bulletinboard.SingleServerWorker; +import meerkat.comm.CommunicationException; +import meerkat.comm.MessageInputStream; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.rest.Constants; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import java.io.IOException; +import java.io.InputStream; + +import static meerkat.bulletinboard.BulletinBoardConstants.BULLETIN_BOARD_SERVER_PATH; +import static meerkat.bulletinboard.BulletinBoardConstants.READ_MESSAGES_PATH; + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + */ +public class SingleServerGetRedundancyWorker extends SingleServerWorker { + + public SingleServerGetRedundancyWorker(String serverAddress, MessageID payload, int maxRetry) { + super(serverAddress, payload, maxRetry); + } + + /** + * This method carries out the actual communication with the server via HTTP Post + * It queries the server for a message with the given ID + * @return TRUE if the message exists in the server and FALSE otherwise + * @throws CommunicationException if the server does not return a valid answer + */ + public Float call() throws CommunicationException{ + + Client client = clientLocal.get(); + + WebTarget webTarget; + Response response; + + MessageFilterList msgFilterList = MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.MSG_ID) + .setId(payload.getID()) + .build() + ).build(); + + // Send request to Server + + // Send request to Server + webTarget = client.target(serverAddress).path(BULLETIN_BOARD_SERVER_PATH).path(READ_MESSAGES_PATH); + InputStream in = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(msgFilterList, Constants.MEDIATYPE_PROTOBUF), InputStream.class); + + MessageInputStream inputStream = null; + + // Retrieve answer + + try { + + inputStream = MessageInputStream.MessageInputStreamFactory.createMessageInputStream(in, BulletinBoardMessage.class); + + if (inputStream.asList().size() > 0){ + // Message exists in the server + return 1.0f; + } + else { + // Message does not exist in the server + return 0.0f; + } + + } catch (Exception e) { + + // Read failed + throw new CommunicationException("Server access failed"); + + } finally { + try { + inputStream.close(); + } catch (IOException ignored) {} + } + + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerPostBatchWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerPostBatchWorker.java new file mode 100644 index 0000000..dfb42a7 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerPostBatchWorker.java @@ -0,0 +1,17 @@ +package meerkat.bulletinboard.workers.singleserver; + +import meerkat.protobuf.BulletinBoardAPI.BatchMessage; + +import static meerkat.bulletinboard.BulletinBoardConstants.POST_BATCH_PATH; + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + * Tries to contact server once and perform a post batch operation + */ +public class SingleServerPostBatchWorker extends SingleServerGenericPostWorker { + + public SingleServerPostBatchWorker(String serverAddress, BatchMessage payload, int maxRetry) { + super(serverAddress, POST_BATCH_PATH, payload, maxRetry); + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerPostMessageWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerPostMessageWorker.java new file mode 100644 index 0000000..454d720 --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerPostMessageWorker.java @@ -0,0 +1,17 @@ +package meerkat.bulletinboard.workers.singleserver; + +import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage; + +import static meerkat.bulletinboard.BulletinBoardConstants.POST_MESSAGE_PATH; + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + * Tries to contact server once and perform a post operation + */ +public class SingleServerPostMessageWorker extends SingleServerGenericPostWorker { + + public SingleServerPostMessageWorker(String serverAddress, BulletinBoardMessage payload, int maxRetry) { + super(serverAddress, POST_MESSAGE_PATH, payload, maxRetry); + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerQuerySyncWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerQuerySyncWorker.java new file mode 100644 index 0000000..3a9873d --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerQuerySyncWorker.java @@ -0,0 +1,59 @@ +package meerkat.bulletinboard.workers.singleserver; + +import meerkat.bulletinboard.SingleServerWorker; +import meerkat.comm.CommunicationException; +import meerkat.protobuf.BulletinBoardAPI.SyncQuery; +import meerkat.protobuf.BulletinBoardAPI.SyncQueryResponse; +import meerkat.rest.Constants; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import static meerkat.bulletinboard.BulletinBoardConstants.BULLETIN_BOARD_SERVER_PATH; +import static meerkat.bulletinboard.BulletinBoardConstants.SYNC_QUERY_PATH; + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + * Tries to contact server once and perform a post operation + */ +public class SingleServerQuerySyncWorker extends SingleServerWorker { + + public SingleServerQuerySyncWorker(String serverAddress, SyncQuery payload, int maxRetry) { + super(serverAddress, payload, maxRetry); + } + + @Override + public SyncQueryResponse call() throws Exception { + + Client client = clientLocal.get(); + + WebTarget webTarget; + Response response; + + // Send request to Server + + webTarget = client.target(serverAddress).path(BULLETIN_BOARD_SERVER_PATH).path(SYNC_QUERY_PATH); + response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(payload, Constants.MEDIATYPE_PROTOBUF)); + + // Retrieve answer + + try { + + // If a BulletinBoardMessageList is returned: the read was successful + return response.readEntity(SyncQueryResponse.class); + + } catch (ProcessingException | IllegalStateException e) { + + // Read failed + throw new CommunicationException("Server access failed"); + + } + finally { + response.close(); + } + + } +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerReadBatchWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerReadBatchWorker.java new file mode 100644 index 0000000..8bc4bcd --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerReadBatchWorker.java @@ -0,0 +1,71 @@ +package meerkat.bulletinboard.workers.singleserver; + +import meerkat.bulletinboard.SingleServerWorker; +import meerkat.comm.CommunicationException; +import meerkat.comm.MessageInputStream; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.rest.Constants; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +import static meerkat.bulletinboard.BulletinBoardConstants.BULLETIN_BOARD_SERVER_PATH; +import static meerkat.bulletinboard.BulletinBoardConstants.READ_BATCH_PATH; + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + */ +public class SingleServerReadBatchWorker extends SingleServerWorker> { + + public SingleServerReadBatchWorker(String serverAddress, BatchQuery payload, int maxRetry) { + super(serverAddress, payload, maxRetry); + } + + /** + * This method carries out the actual communication with the server via HTTP Post + * Upon successful retrieval from the server the method returns the received values + * @return the complete batch as read from the server + * @throws CommunicationException if the server's response is invalid + */ + public List call() throws CommunicationException{ + + Client client = clientLocal.get(); + + WebTarget webTarget; + + // Get the batch data + + webTarget = client.target(serverAddress).path(BULLETIN_BOARD_SERVER_PATH).path(READ_BATCH_PATH); + InputStream in = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(payload, Constants.MEDIATYPE_PROTOBUF), InputStream.class); + + MessageInputStream inputStream = null; + + try { + + inputStream = MessageInputStream.MessageInputStreamFactory.createMessageInputStream(in, BatchChunk.class); + + return inputStream.asList(); + + } catch (IOException | InvocationTargetException e) { + + // Read failed + throw new CommunicationException("Could not contact the server or server returned illegal result"); + + } catch (NoSuchMethodException | IllegalAccessException e) { + + throw new CommunicationException("MessageInputStream error"); + + } finally { + try { + inputStream.close(); + } catch (IOException ignored) {} + } + + } + +} diff --git a/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerReadMessagesWorker.java b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerReadMessagesWorker.java new file mode 100644 index 0000000..d8525ab --- /dev/null +++ b/bulletin-board-client/src/main/java/meerkat/bulletinboard/workers/singleserver/SingleServerReadMessagesWorker.java @@ -0,0 +1,76 @@ +package meerkat.bulletinboard.workers.singleserver; + +import meerkat.bulletinboard.SingleServerWorker; +import meerkat.comm.CommunicationException; +import meerkat.comm.MessageInputStream; +import meerkat.protobuf.BulletinBoardAPI; +import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessageList; +import meerkat.protobuf.BulletinBoardAPI.MessageFilterList; +import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage; +import meerkat.rest.Constants; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +import static meerkat.bulletinboard.BulletinBoardConstants.BULLETIN_BOARD_SERVER_PATH; +import static meerkat.bulletinboard.BulletinBoardConstants.READ_MESSAGES_PATH; + +/** + * Created by Arbel Deutsch Peled on 27-Dec-15. + */ +public class SingleServerReadMessagesWorker extends SingleServerWorker> { + + public SingleServerReadMessagesWorker(String serverAddress, MessageFilterList payload, int maxRetry) { + super(serverAddress, payload, maxRetry); + } + + /** + * This method carries out the actual communication with the server via HTTP Post + * Upon successful retrieval from the server the method returns the received values + * @return The list of messages returned by the server + * @throws CommunicationException if the server's response is invalid + */ + public List call() throws CommunicationException{ + + Client client = clientLocal.get(); + + WebTarget webTarget; + + // Send request to Server + webTarget = client.target(serverAddress).path(BULLETIN_BOARD_SERVER_PATH).path(READ_MESSAGES_PATH); + InputStream in = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(payload, Constants.MEDIATYPE_PROTOBUF), InputStream.class); + + MessageInputStream inputStream = null; + + try { + + inputStream = MessageInputStream.MessageInputStreamFactory.createMessageInputStream(in, BulletinBoardMessage.class); + + return inputStream.asList(); + + } catch (IOException | InvocationTargetException e) { + + // Read failed + throw new CommunicationException("Could not contact the server or server returned illegal result"); + + } catch (NoSuchMethodException | IllegalAccessException e) { + + throw new CommunicationException("MessageInputStream error"); + + } finally { + try { + inputStream.close(); + } catch (IOException ignored) {} + } + + } + +} diff --git a/bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java b/bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java deleted file mode 100644 index dda76c7..0000000 --- a/bulletin-board-client/src/test/java/BulletinBoardClientIntegrationTest.java +++ /dev/null @@ -1,214 +0,0 @@ -import com.google.protobuf.ByteString; -import meerkat.bulletinboard.BulletinBoardClient; -import meerkat.bulletinboard.BulletinBoardClient.ClientCallback; -import meerkat.bulletinboard.ThreadedBulletinBoardClient; -import meerkat.protobuf.BulletinBoardAPI.*; -import meerkat.protobuf.Crypto; - -import meerkat.protobuf.Voting.*; -import meerkat.util.BulletinBoardMessageComparator; - -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.*; -import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.number.OrderingComparison.*; - -import java.util.*; -import java.util.concurrent.Semaphore; - -/** - * Created by Arbel Deutsch Peled on 05-Dec-15. - */ -public class BulletinBoardClientIntegrationTest { - - Semaphore jobSemaphore; - Vector thrown; - - private class PostCallback implements ClientCallback{ - - @Override - public void handleCallback(Object msg) { - System.err.println("Post operation completed"); - jobSemaphore.release(); - } - - @Override - public void handleFailure(Throwable t) { - thrown.add(t); - jobSemaphore.release(); - } - } - - private class RedundancyCallback implements ClientCallback{ - - private float minRedundancy; - - public RedundancyCallback(float minRedundancy) { - this.minRedundancy = minRedundancy; - } - - @Override - public void handleCallback(Float redundancy) { - System.err.println("Redundancy found is: " + redundancy); - jobSemaphore.release(); - assertThat(redundancy, greaterThanOrEqualTo(minRedundancy)); - } - - @Override - public void handleFailure(Throwable t) { - thrown.add(t); - jobSemaphore.release(); - } - } - - private class ReadCallback implements ClientCallback>{ - - private List expectedMsgList; - - public ReadCallback(List expectedMsgList) { - this.expectedMsgList = expectedMsgList; - } - - @Override - public void handleCallback(List messages) { - - System.err.println(messages); - jobSemaphore.release(); - - BulletinBoardMessageComparator msgComparator = new BulletinBoardMessageComparator(); - - assertThat(messages.size(), is(expectedMsgList.size())); - - Iterator expectedMessageIterator = expectedMsgList.iterator(); - Iterator receivedMessageIterator = messages.iterator(); - - while (expectedMessageIterator.hasNext()) { - assertThat(msgComparator.compare(expectedMessageIterator.next(), receivedMessageIterator.next()), is(0)); - } - - } - - @Override - public void handleFailure(Throwable t) { - thrown.add(t); - jobSemaphore.release(); - } - } - - private BulletinBoardClient bulletinBoardClient; - - private PostCallback postCallback; - private RedundancyCallback redundancyCallback; - private ReadCallback readCallback; - - private static String PROP_GETTY_URL = "gretty.httpBaseURI"; - private static String DEFAULT_BASE_URL = "http://localhost:8081"; - private static String BASE_URL = System.getProperty(PROP_GETTY_URL, DEFAULT_BASE_URL); - - @Before - public void init(){ - - bulletinBoardClient = new ThreadedBulletinBoardClient(); - - List testDB = new LinkedList(); - testDB.add(BASE_URL); - - bulletinBoardClient.init(BulletinBoardClientParams.newBuilder() - .addBulletinBoardAddress("http://localhost:8081") - .setMinRedundancy((float) 1.0) - .build()); - - postCallback = new PostCallback(); - redundancyCallback = new RedundancyCallback((float) 1.0); - - thrown = new Vector<>(); - jobSemaphore = new Semaphore(0); - - } - - @Test - public void postTest() { - - byte[] b1 = {(byte) 1, (byte) 2, (byte) 3, (byte) 4}; - byte[] b2 = {(byte) 11, (byte) 12, (byte) 13, (byte) 14}; - byte[] b3 = {(byte) 21, (byte) 22, (byte) 23, (byte) 24}; - byte[] b4 = {(byte) 4, (byte) 5, (byte) 100, (byte) -50, (byte) 0}; - - BulletinBoardMessage msg; - - MessageFilterList filterList; - List msgList; - - MessageID messageID; - - Comparator msgComparator = new BulletinBoardMessageComparator(); - - msg = BulletinBoardMessage.newBuilder() - .setMsg(UnsignedBulletinBoardMessage.newBuilder() - .addTag("Signature") - .addTag("Trustee") - .setData(ByteString.copyFrom(b1)) - .build()) - .addSig(Crypto.Signature.newBuilder() - .setType(Crypto.SignatureType.DSA) - .setData(ByteString.copyFrom(b2)) - .setSignerId(ByteString.copyFrom(b3)) - .build()) - .addSig(Crypto.Signature.newBuilder() - .setType(Crypto.SignatureType.ECDSA) - .setData(ByteString.copyFrom(b3)) - .setSignerId(ByteString.copyFrom(b2)) - .build()) - .build(); - - messageID = bulletinBoardClient.postMessage(msg,postCallback); - - try { - jobSemaphore.acquire(); - } catch (InterruptedException e) { - System.err.println(e.getCause() + " " + e.getMessage()); - } - - bulletinBoardClient.getRedundancy(messageID,redundancyCallback); - - filterList = MessageFilterList.newBuilder() - .addFilter( - MessageFilter.newBuilder() - .setType(FilterType.TAG) - .setTag("Signature") - .build() - ) - .addFilter( - MessageFilter.newBuilder() - .setType(FilterType.TAG) - .setTag("Trustee") - .build() - ) - .build(); - - msgList = new LinkedList(); - msgList.add(msg); - - readCallback = new ReadCallback(msgList); - - bulletinBoardClient.readMessages(filterList, readCallback); - try { - jobSemaphore.acquire(2); - } catch (InterruptedException e) { - System.err.println(e.getCause() + " " + e.getMessage()); - } - - bulletinBoardClient.close(); - - for (Throwable t : thrown) { - System.err.println(t.getMessage()); - } - if (thrown.size() > 0) { - assert false; - } - - } - -} diff --git a/bulletin-board-client/src/test/java/meerkat/bulletinboard/BulletinBoardSynchronizerTest.java b/bulletin-board-client/src/test/java/meerkat/bulletinboard/BulletinBoardSynchronizerTest.java new file mode 100644 index 0000000..19e7ae5 --- /dev/null +++ b/bulletin-board-client/src/test/java/meerkat/bulletinboard/BulletinBoardSynchronizerTest.java @@ -0,0 +1,318 @@ +package meerkat.bulletinboard; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.protobuf.*; +import com.google.protobuf.Timestamp; + +import static meerkat.bulletinboard.BulletinBoardSynchronizer.SyncStatus; + +import meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer; +import meerkat.bulletinboard.sqlserver.H2QueryProvider; + +import meerkat.comm.CommunicationException; +import meerkat.crypto.concrete.ECDSASignature; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.util.BulletinBoardMessageComparator; +import meerkat.util.BulletinBoardMessageGenerator; +import org.junit.*; + +import java.io.IOException; +import java.io.InputStream; +import java.security.*; +import java.security.cert.CertificateException; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.fail; + +/** + * Created by Arbel on 6/1/2016. + */ +public class BulletinBoardSynchronizerTest { + + private static final String REMOTE_SERVER_ADDRESS = "remoteDB"; + private static final String LOCAL_SERVER_ADDRESS = "localDB"; + private static int testCount; + + private static final int THREAD_NUM = 3; + private static final int SUBSCRIPTION_INTERVAL = 1000; + + private static final int SYNC_SLEEP_INTERVAL = 500; + private static final int SYNC_WAIT_CAP = 1000; + + private DeletableSubscriptionBulletinBoardClient localClient; + private AsyncBulletinBoardClient remoteClient; + + private BulletinBoardSynchronizer synchronizer; + + private static BulletinBoardMessageGenerator messageGenerator; + private static BulletinBoardMessageComparator messageComparator; + + private static String KEYFILE_EXAMPLE = "/certs/enduser-certs/user1-key-with-password-secret.p12"; + private static String KEYFILE_PASSWORD1 = "secret"; + private static String CERT1_PEM_EXAMPLE = "/certs/enduser-certs/user1.crt"; + + private static BulletinBoardSignature[] signers; + private static ByteString[] signerIDs; + + private Semaphore semaphore; + private List thrown; + + @BeforeClass + public static void build() { + + messageGenerator = new BulletinBoardMessageGenerator(new Random(0)); + messageComparator = new BulletinBoardMessageComparator(); + + signers = new BulletinBoardSignature[1]; + signerIDs = new ByteString[1]; + + signers[0] = new GenericBulletinBoardSignature(new ECDSASignature()); + signerIDs[0] = signers[0].getSignerID(); + + InputStream keyStream = BulletinBoardSynchronizerTest.class.getResourceAsStream(KEYFILE_EXAMPLE); + char[] password = KEYFILE_PASSWORD1.toCharArray(); + + try { + + KeyStore.Builder keyStoreBuilder = signers[0].getPKCS12KeyStoreBuilder(keyStream, password); + + signers[0].loadSigningCertificate(keyStoreBuilder); + + signers[0].loadVerificationCertificates(BulletinBoardSynchronizerTest.class.getResourceAsStream(CERT1_PEM_EXAMPLE)); + + } catch (IOException e) { + System.err.println("Failed reading from signature file " + e.getMessage()); + fail("Failed reading from signature file " + e.getMessage()); + } catch (CertificateException e) { + System.err.println("Failed reading certificate " + e.getMessage()); + fail("Failed reading certificate " + e.getMessage()); + } catch (KeyStoreException e) { + System.err.println("Failed reading keystore " + e.getMessage()); + fail("Failed reading keystore " + e.getMessage()); + } catch (NoSuchAlgorithmException e) { + System.err.println("Couldn't find signing algorithm " + e.getMessage()); + fail("Couldn't find signing algorithm " + e.getMessage()); + } catch (UnrecoverableKeyException e) { + System.err.println("Couldn't find signing key " + e.getMessage()); + fail("Couldn't find signing key " + e.getMessage()); + } + + signerIDs[0] = signers[0].getSignerID(); + + testCount = 0; + + } + + @Before + public void init() throws CommunicationException { + + DeletableBulletinBoardServer remoteServer = new BulletinBoardSQLServer(new H2QueryProvider(REMOTE_SERVER_ADDRESS + testCount)); + remoteServer.init(); + + remoteClient = new LocalBulletinBoardClient( + remoteServer, + THREAD_NUM, + SUBSCRIPTION_INTERVAL); + + DeletableBulletinBoardServer localServer = new BulletinBoardSQLServer(new H2QueryProvider(LOCAL_SERVER_ADDRESS + testCount)); + localServer.init(); + + localClient = new LocalBulletinBoardClient( + localServer, + THREAD_NUM, + SUBSCRIPTION_INTERVAL); + + synchronizer = new SimpleBulletinBoardSynchronizer(SYNC_SLEEP_INTERVAL, SYNC_WAIT_CAP); + synchronizer.init(localClient, remoteClient); + + semaphore = new Semaphore(0); + thrown = new LinkedList<>(); + + testCount++; + + } + + private class SyncStatusCallback implements FutureCallback { + + private final SyncStatus statusToWaitFor; + private AtomicBoolean stillWaiting; + + public SyncStatusCallback(SyncStatus statusToWaitFor) { + this.statusToWaitFor = statusToWaitFor; + stillWaiting = new AtomicBoolean(true); + } + + @Override + public void onSuccess(SyncStatus result) { + + if (result == statusToWaitFor && stillWaiting.compareAndSet(true, false)){ + semaphore.release(); + } + + } + + @Override + public void onFailure(Throwable t) { + thrown.add(t); + if (stillWaiting.compareAndSet(true,false)) { + semaphore.release(); + } + } + } + + private class MessageCountCallback implements FutureCallback { + + private int[] expectedCounts; + private int currentIteration; + + public MessageCountCallback(int[] expectedCounts) { + this.expectedCounts = expectedCounts; + this.currentIteration = 0; + } + + @Override + public void onSuccess(Integer result) { + + if (currentIteration < expectedCounts.length){ + if (result != expectedCounts[currentIteration]){ + onFailure(new AssertionError("Wrong message count. Expected " + expectedCounts[currentIteration] + " but received " + result)); + currentIteration = expectedCounts.length; + return; + } + } + + currentIteration++; + + if (currentIteration == expectedCounts.length) + semaphore.release(); + } + + @Override + public void onFailure(Throwable t) { + thrown.add(t); + semaphore.release(); + } + } + + @Test + public void testSync() throws SignatureException, CommunicationException, InterruptedException { + + Timestamp timestamp = Timestamp.newBuilder() + .setSeconds(15252162) + .setNanos(85914) + .build(); + + BulletinBoardMessage msg = messageGenerator.generateRandomMessage(signers, timestamp, 10, 10); + + MessageID msgID = localClient.postMessage(msg); + + timestamp = Timestamp.newBuilder() + .setSeconds(51511653) + .setNanos(3625) + .build(); + + BulletinBoardMessage batchMessage = messageGenerator.generateRandomMessage(signers,timestamp, 100, 10); + + MessageID batchMsgID = localClient.postAsBatch(batchMessage, 10); + + BulletinBoardMessage test = localClient.readMessage(batchMsgID); + + BulletinBoardMessage stub = localClient.readMessages(MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.MSG_ID) + .setId(batchMsgID.getID()) + .build()) + .build()).get(0); + + BulletinBoardMessage test2 = localClient.readBatchData(stub); + + synchronizer.subscribeToSyncStatus(new SyncStatusCallback(SyncStatus.SYNCHRONIZED)); + + int[] expectedCounts = {2,0}; + synchronizer.subscribeToRemainingMessagesCount(new MessageCountCallback(expectedCounts)); + + Thread syncThread = new Thread(synchronizer); + syncThread.start(); + + if (!semaphore.tryAcquire(2, 4000, TimeUnit.MILLISECONDS)) { + thrown.add(new TimeoutException("Timeout occurred while waiting for synchronizer to sync.")); + } + + synchronizer.stop(); + syncThread.join(); + + if (thrown.size() > 0) { + for (Throwable t : thrown) + System.err.println(t.getMessage()); + assertThat("Exception thrown by Synchronizer: " + thrown.get(0).getMessage(), false); + } + + List msgList = remoteClient.readMessages(MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.MSG_ID) + .setId(msgID.getID()) + .build()) + .build()); + + assertThat("Wrong number of messages returned.", msgList.size() == 1); + assertThat("Returned message is not equal to original one", messageComparator.compare(msgList.get(0),msg) == 0); + + BulletinBoardMessage returnedBatchMsg = remoteClient.readMessage(batchMsgID); + + assertThat("Returned batch does not equal original one.", messageComparator.compare(returnedBatchMsg, batchMessage) == 0); + + } + + @Test + public void testServerError() throws SignatureException, CommunicationException, InterruptedException { + + Timestamp timestamp = Timestamp.newBuilder() + .setSeconds(945736256) + .setNanos(276788) + .build(); + + BulletinBoardMessage msg = messageGenerator.generateRandomMessage(signers, timestamp, 10, 10); + + remoteClient.close(); + + synchronizer.subscribeToSyncStatus(new SyncStatusCallback(SyncStatus.SERVER_ERROR)); + + localClient.postMessage(msg); + + Thread thread = new Thread(synchronizer); + + thread.start(); + + if (!semaphore.tryAcquire(4000, TimeUnit.MILLISECONDS)) { + thrown.add(new TimeoutException("Timeout occurred while waiting for synchronizer to sync.")); + } + + synchronizer.stop(); + thread.join(); + + } + + @After + public void close() { + + if (thrown.size() > 0) { + for (Throwable t : thrown) { + System.err.println(t.getMessage()); + } + assertThat("Exception thrown by Synchronizer: " + thrown.get(0).getMessage(), false); + } + + synchronizer.stop(); + localClient.close(); + remoteClient.close(); + + } + +} diff --git a/bulletin-board-client/src/test/java/meerkat/bulletinboard/CachedBulletinBoardClientTest.java b/bulletin-board-client/src/test/java/meerkat/bulletinboard/CachedBulletinBoardClientTest.java new file mode 100644 index 0000000..8bae8c2 --- /dev/null +++ b/bulletin-board-client/src/test/java/meerkat/bulletinboard/CachedBulletinBoardClientTest.java @@ -0,0 +1,111 @@ +package meerkat.bulletinboard; + +import meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer; +import meerkat.bulletinboard.sqlserver.H2QueryProvider; +import meerkat.comm.CommunicationException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.security.SignatureException; +import java.util.LinkedList; +import java.util.List; + +/** + * Created by Arbel on 6/27/2016. + */ +public class CachedBulletinBoardClientTest { + + private static final int THREAD_NUM = 3; + + private static final String LOCAL_DB_NAME = "localDB"; + private static final String REMOTE_DB_NAME = "remoteDB"; + private static final String QUEUE_DB_NAME = "queueDB"; + + private static final int SUBSRCIPTION_DELAY = 500; + private static final int SYNC_DELAY = 500; + + // Testers + private CachedBulletinBoardClient cachedClient; + private GenericBulletinBoardClientTester clientTest; + private GenericSubscriptionClientTester subscriptionTester; + + public CachedBulletinBoardClientTest() throws CommunicationException { + + DeletableBulletinBoardServer localServer = new BulletinBoardSQLServer(new H2QueryProvider(LOCAL_DB_NAME)); + localServer.init(); + LocalBulletinBoardClient localClient = new LocalBulletinBoardClient(localServer, THREAD_NUM, SUBSRCIPTION_DELAY); + + DeletableBulletinBoardServer remoteServer = new BulletinBoardSQLServer(new H2QueryProvider(REMOTE_DB_NAME)); + remoteServer.init(); + LocalBulletinBoardClient remoteClient = new LocalBulletinBoardClient(remoteServer, THREAD_NUM, SUBSRCIPTION_DELAY); + + DeletableBulletinBoardServer queueServer = new BulletinBoardSQLServer(new H2QueryProvider(QUEUE_DB_NAME)); + queueServer.init(); + LocalBulletinBoardClient queueClient = new LocalBulletinBoardClient(queueServer, THREAD_NUM, SUBSRCIPTION_DELAY); + + List clientList = new LinkedList<>(); + clientList.add(remoteClient); + + BulletinBoardSubscriber subscriber = new ThreadedBulletinBoardSubscriber(clientList, localClient); + + cachedClient = new CachedBulletinBoardClient(localClient, remoteClient, subscriber, queueClient, SYNC_DELAY, SYNC_DELAY); + subscriptionTester = new GenericSubscriptionClientTester(cachedClient); + clientTest = new GenericBulletinBoardClientTester(cachedClient, 87351); + + } + + // Test methods + + /** + * Takes care of initializing the client and the test resources + */ + @Before + public void init(){ + + clientTest.init(); + + } + + /** + * Closes the client and makes sure the test fails when an exception occurred in a separate thread + */ + + @After + public void close() { + + cachedClient.close(); + clientTest.close(); + + } + + @Test + public void testPost() { + + clientTest.testPost(); + + } + + @Test + public void testBatchPost() throws CommunicationException, SignatureException, InterruptedException { + + clientTest.testBatchPost(); + } + + @Test + public void testCompleteBatchPost() throws CommunicationException, SignatureException, InterruptedException { + + clientTest.testCompleteBatchPost(); + + } + + @Test + public void testSubscription() throws SignatureException, CommunicationException { + +// subscriptionTester.init(); +// subscriptionTester.subscriptionTest(); +// subscriptionTester.close(); + + } + +} diff --git a/bulletin-board-client/src/test/java/meerkat/bulletinboard/GenericBulletinBoardClientTester.java b/bulletin-board-client/src/test/java/meerkat/bulletinboard/GenericBulletinBoardClientTester.java new file mode 100644 index 0000000..7ded2e4 --- /dev/null +++ b/bulletin-board-client/src/test/java/meerkat/bulletinboard/GenericBulletinBoardClientTester.java @@ -0,0 +1,497 @@ +package meerkat.bulletinboard; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.protobuf.ByteString; +import com.google.protobuf.Timestamp; +import meerkat.comm.CommunicationException; +import meerkat.crypto.concrete.ECDSASignature; +import meerkat.crypto.concrete.SHA256Digest; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.protobuf.Crypto; +import meerkat.util.BulletinBoardMessageComparator; +import meerkat.util.BulletinBoardMessageGenerator; +import meerkat.util.BulletinBoardUtils; +import meerkat.bulletinboard.AsyncBulletinBoardClient.BatchIdentifier; + +import java.io.IOException; +import java.io.InputStream; +import java.security.*; +import java.security.cert.CertificateException; +import java.util.*; +import java.util.concurrent.Semaphore; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.number.OrderingComparison.greaterThanOrEqualTo; +import static org.junit.Assert.*; + +/** + * Created by Arbel Deutsch Peled on 05-Dec-15. + */ +public class GenericBulletinBoardClientTester { + + // Signature resources + + private BulletinBoardSignature signers[]; + private ByteString[] signerIDs; + + private static String KEYFILE_EXAMPLE = "/certs/enduser-certs/user1-key-with-password-secret.p12"; + private static String KEYFILE_EXAMPLE3 = "/certs/enduser-certs/user3-key-with-password-shh.p12"; + + private static String KEYFILE_PASSWORD1 = "secret"; + private static String KEYFILE_PASSWORD3 = "shh"; + + private static String CERT1_PEM_EXAMPLE = "/certs/enduser-certs/user1.crt"; + private static String CERT3_PEM_EXAMPLE = "/certs/enduser-certs/user3.crt"; + + // Client and callbacks + + private AsyncBulletinBoardClient bulletinBoardClient; + + private PostCallback postCallback; + + private RedundancyCallback redundancyCallback; + private ReadCallback readCallback; + + // Sync and misc + + private Semaphore jobSemaphore; + private Vector thrown; + private Random random; + private BulletinBoardMessageGenerator generator; + + private BulletinBoardDigest digest; + + // Constructor + + public GenericBulletinBoardClientTester(AsyncBulletinBoardClient bulletinBoardClient, int seed){ + + this.bulletinBoardClient = bulletinBoardClient; + + signers = new GenericBulletinBoardSignature[2]; + signerIDs = new ByteString[signers.length]; + signers[0] = new GenericBulletinBoardSignature(new ECDSASignature()); + signers[1] = new GenericBulletinBoardSignature(new ECDSASignature()); + + InputStream keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE); + char[] password = KEYFILE_PASSWORD1.toCharArray(); + + KeyStore.Builder keyStoreBuilder; + try { + keyStoreBuilder = signers[0].getPKCS12KeyStoreBuilder(keyStream, password); + + signers[0].loadSigningCertificate(keyStoreBuilder); + + signers[0].loadVerificationCertificates(getClass().getResourceAsStream(CERT1_PEM_EXAMPLE)); + + keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE3); + password = KEYFILE_PASSWORD3.toCharArray(); + + keyStoreBuilder = signers[1].getPKCS12KeyStoreBuilder(keyStream, password); + signers[1].loadSigningCertificate(keyStoreBuilder); + + signers[1].loadVerificationCertificates(getClass().getResourceAsStream(CERT3_PEM_EXAMPLE)); + + for (int i = 0 ; i < signers.length ; i++) { + signerIDs[i] = signers[i].getSignerID(); + } + + } catch (IOException e) { + System.err.println("Failed reading from signature file " + e.getMessage()); + fail("Failed reading from signature file " + e.getMessage()); + } catch (CertificateException e) { + System.err.println("Failed reading certificate " + e.getMessage()); + fail("Failed reading certificate " + e.getMessage()); + } catch (KeyStoreException e) { + System.err.println("Failed reading keystore " + e.getMessage()); + fail("Failed reading keystore " + e.getMessage()); + } catch (NoSuchAlgorithmException e) { + System.err.println("Couldn't find signing algorithm " + e.getMessage()); + fail("Couldn't find signing algorithm " + e.getMessage()); + } catch (UnrecoverableKeyException e) { + System.err.println("Couldn't find signing key " + e.getMessage()); + fail("Couldn't find signing key " + e.getMessage()); + } + + this.random = new Random(seed); + this.generator = new BulletinBoardMessageGenerator(random); + this.digest = new GenericBulletinBoardDigest(new SHA256Digest()); + + } + + // Callback definitions + + protected void genericHandleFailure(Throwable t){ + System.err.println(t.getCause() + " " + t.getMessage()); + thrown.add(t); + jobSemaphore.release(); + } + + private class PostCallback implements FutureCallback{ + + private boolean isAssert; + private boolean assertValue; + + public PostCallback() { + this(false); + } + + public PostCallback(boolean isAssert) { + this(isAssert,true); + } + + public PostCallback(boolean isAssert, boolean assertValue) { + this.isAssert = isAssert; + this.assertValue = assertValue; + } + + @Override + public void onSuccess(Boolean msg) { + + System.err.println("Post operation completed"); + + if (isAssert) { + if (assertValue && !msg) { + genericHandleFailure(new AssertionError("Post operation failed")); + } else if (!assertValue && msg){ + genericHandleFailure(new AssertionError("Post operation succeeded unexpectedly")); + } else { + jobSemaphore.release(); + } + } else { + jobSemaphore.release(); + } + + } + + @Override + public void onFailure(Throwable t) { + genericHandleFailure(t); + } + } + + private class RedundancyCallback implements FutureCallback{ + + private float minRedundancy; + + public RedundancyCallback(float minRedundancy) { + this.minRedundancy = minRedundancy; + } + + @Override + public void onSuccess(Float redundancy) { + System.err.println("Redundancy found is: " + redundancy); + jobSemaphore.release(); + assertThat(redundancy, greaterThanOrEqualTo(minRedundancy)); + } + + @Override + public void onFailure(Throwable t) { + genericHandleFailure(t); + } + } + + private class ReadCallback implements FutureCallback>{ + + private List expectedMsgList; + + public ReadCallback(List expectedMsgList) { + this.expectedMsgList = expectedMsgList; + } + + @Override + public void onSuccess(List messages) { + + System.err.println(messages); + jobSemaphore.release(); + + BulletinBoardMessageComparator msgComparator = new BulletinBoardMessageComparator(); + + assertThat(messages.size(), is(expectedMsgList.size())); + + Iterator expectedMessageIterator = expectedMsgList.iterator(); + Iterator receivedMessageIterator = messages.iterator(); + + while (expectedMessageIterator.hasNext()) { + assertThat(msgComparator.compare(expectedMessageIterator.next(), receivedMessageIterator.next()), is(0)); + } + + } + + @Override + public void onFailure(Throwable t) { + genericHandleFailure(t); + } + } + + private class ReadBatchCallback implements FutureCallback{ + + private BulletinBoardMessage expectedMsg; + + public ReadBatchCallback(BulletinBoardMessage expectedMsg) { + this.expectedMsg = expectedMsg; + } + + @Override + public void onSuccess(BulletinBoardMessage msg) { + + BulletinBoardMessageComparator msgComparator = new BulletinBoardMessageComparator(); + + if (msgComparator.compare(msg, expectedMsg) != 0) { + genericHandleFailure(new AssertionError("Batch read returned different message.\nExpected:" + expectedMsg + "\nRecieved:" + msg + "\n")); + } else { + jobSemaphore.release(); + } + + } + + @Override + public void onFailure(Throwable t) { + genericHandleFailure(t); + } + } + + // Test methods + + /** + * Takes care of initializing the client and the test resources + */ + public void init(){ + + random = new Random(0); // We use insecure randomness in tests for repeatability + + postCallback = new PostCallback(); + redundancyCallback = new RedundancyCallback((float) 1.0); + + thrown = new Vector<>(); + jobSemaphore = new Semaphore(0); + + } + + /** + * Closes the client and makes sure the test fails when an exception occurred in a separate thread + */ + + public void close() { + + if (thrown.size() > 0) { + + for (Throwable t : thrown){ + System.err.println(t.getMessage()); + } + + assert false; + + } + + } + + /** + * Tests the standard post, redundancy and read methods + */ + public void testPost() { + + byte[] b1 = {(byte) 1, (byte) 2, (byte) 3, (byte) 4}; + byte[] b2 = {(byte) 11, (byte) 12, (byte) 13, (byte) 14}; + byte[] b3 = {(byte) 21, (byte) 22, (byte) 23, (byte) 24}; + + BulletinBoardMessage msg; + + MessageFilterList filterList; + List msgList; + + MessageID messageID; + + msg = BulletinBoardMessage.newBuilder() + .setMsg(UnsignedBulletinBoardMessage.newBuilder() + .addTag("Signature") + .addTag("Trustee") + .setData(ByteString.copyFrom(b1)) + .setTimestamp(Timestamp.newBuilder() + .setSeconds(20) + .setNanos(30) + .build()) + .build()) + .addSig(Crypto.Signature.newBuilder() + .setType(Crypto.SignatureType.DSA) + .setData(ByteString.copyFrom(b2)) + .setSignerId(ByteString.copyFrom(b3)) + .build()) + .addSig(Crypto.Signature.newBuilder() + .setType(Crypto.SignatureType.ECDSA) + .setData(ByteString.copyFrom(b3)) + .setSignerId(ByteString.copyFrom(b2)) + .build()) + .build(); + + messageID = bulletinBoardClient.postMessage(msg,postCallback); + + try { + jobSemaphore.acquire(); + } catch (InterruptedException e) { + System.err.println(e.getCause() + " " + e.getMessage()); + } + + bulletinBoardClient.getRedundancy(messageID,redundancyCallback); + + filterList = MessageFilterList.newBuilder() + .addFilter( + MessageFilter.newBuilder() + .setType(FilterType.TAG) + .setTag("Signature") + .build() + ) + .addFilter( + MessageFilter.newBuilder() + .setType(FilterType.TAG) + .setTag("Trustee") + .build() + ) + .build(); + + msgList = new LinkedList<>(); + msgList.add(msg); + + readCallback = new ReadCallback(msgList); + + bulletinBoardClient.readMessages(filterList, readCallback); + try { + jobSemaphore.acquire(2); + } catch (InterruptedException e) { + System.err.println(e.getCause() + " " + e.getMessage()); + } + + } + + /** + * Tests posting a batch by parts + * @throws CommunicationException, SignatureException, InterruptedException + */ + public void testBatchPost() throws CommunicationException, SignatureException, InterruptedException { + + final int BATCH_LENGTH = 100; + final int CHUNK_SIZE = 10; + final int TAG_NUM = 10; + + final Timestamp timestamp = Timestamp.newBuilder() + .setSeconds(141515) + .setNanos(859018) + .build(); + + final BulletinBoardMessage msg = generator.generateRandomMessage(signers, timestamp, BATCH_LENGTH, TAG_NUM); + + // Begin batch + + bulletinBoardClient.beginBatch(msg.getMsg().getTagList(), new FutureCallback() { + + @Override + public void onSuccess(final BatchIdentifier identifier) { + + bulletinBoardClient.postBatchData(identifier, BulletinBoardUtils.breakToBatch(msg, CHUNK_SIZE), new FutureCallback() { + + @Override + public void onSuccess(Boolean result) { + + bulletinBoardClient.closeBatch(identifier, msg.getMsg().getTimestamp(), msg.getSigList(), new FutureCallback() { + @Override + public void onSuccess(Boolean result) { + jobSemaphore.release(); + } + + @Override + public void onFailure(Throwable t) { + genericHandleFailure(t); + } + }); + + } + + @Override + public void onFailure(Throwable t) { + genericHandleFailure(t); + } + + }); + + } + + @Override + public void onFailure(Throwable t) { + genericHandleFailure(t); + } + + }); + + jobSemaphore.acquire(); + + digest.reset(); + digest.update(msg); + + bulletinBoardClient.readMessage(digest.digestAsMessageID(), new ReadBatchCallback(msg)); + + jobSemaphore.acquire(); + + } + + /** + * Posts a complete batch message + * Checks reading of the message in two parts + * @throws CommunicationException, SignatureException, InterruptedException + */ + public void testCompleteBatchPost() throws CommunicationException, SignatureException, InterruptedException { + + final int BATCH_LENGTH = 100; + final int CHUNK_SIZE = 99; + final int TAG_NUM = 8; + + final Timestamp timestamp = Timestamp.newBuilder() + .setSeconds(7776151) + .setNanos(252616) + .build(); + + final BulletinBoardMessage msg = generator.generateRandomMessage(signers, timestamp, BATCH_LENGTH, TAG_NUM); + + // Post batch + + MessageID msgID = bulletinBoardClient.postAsBatch(msg, CHUNK_SIZE, postCallback); + + jobSemaphore.acquire(); + + // Read batch + + MessageFilterList filterList = MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.MSG_ID) + .setId(msgID.getID()) + .build()) + .build(); + + bulletinBoardClient.readMessages(filterList, new FutureCallback>() { + + @Override + public void onSuccess(List msgList) { + + if (msgList.size() != 1) { + + genericHandleFailure(new AssertionError("Wrong number of stubs returned. Expected: 1; Found: " + msgList.size())); + + } else { + + BulletinBoardMessage retrievedMsg = msgList.get(0); + bulletinBoardClient.readBatchData(retrievedMsg, new ReadBatchCallback(msg)); + + } + + } + + @Override + public void onFailure(Throwable t) { + genericHandleFailure(t); + } + + }); + + jobSemaphore.acquire(); + + } + +} diff --git a/bulletin-board-client/src/test/java/meerkat/bulletinboard/GenericSubscriptionClientTester.java b/bulletin-board-client/src/test/java/meerkat/bulletinboard/GenericSubscriptionClientTester.java new file mode 100644 index 0000000..6d02914 --- /dev/null +++ b/bulletin-board-client/src/test/java/meerkat/bulletinboard/GenericSubscriptionClientTester.java @@ -0,0 +1,226 @@ +package meerkat.bulletinboard; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.protobuf.ByteString; +import com.google.protobuf.Timestamp; +import meerkat.comm.CommunicationException; +import meerkat.crypto.concrete.ECDSASignature; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.util.BulletinBoardMessageComparator; +import meerkat.util.BulletinBoardMessageGenerator; + +import java.io.IOException; +import java.io.InputStream; +import java.security.*; +import java.security.cert.CertificateException; +import java.util.*; +import java.util.concurrent.Semaphore; + +import static org.junit.Assert.fail; + +/** + * Created by Arbel Deutsch Peled on 22-Mar-16. + */ +public class GenericSubscriptionClientTester { + + private BulletinBoardSignature signers[]; + private ByteString[] signerIDs; + + private static String KEYFILE_EXAMPLE = "/certs/enduser-certs/user1-key-with-password-secret.p12"; + private static String KEYFILE_EXAMPLE3 = "/certs/enduser-certs/user3-key-with-password-shh.p12"; + + private static String KEYFILE_PASSWORD1 = "secret"; + private static String KEYFILE_PASSWORD3 = "shh"; + + private static String CERT1_PEM_EXAMPLE = "/certs/enduser-certs/user1.crt"; + private static String CERT3_PEM_EXAMPLE = "/certs/enduser-certs/user3.crt"; + + private SubscriptionBulletinBoardClient bulletinBoardClient; + + private Random random; + private BulletinBoardMessageGenerator generator; + + private Semaphore jobSemaphore; + private Vector thrown; + + public GenericSubscriptionClientTester(SubscriptionBulletinBoardClient bulletinBoardClient){ + + this.bulletinBoardClient = bulletinBoardClient; + + signers = new BulletinBoardSignature[2]; + signerIDs = new ByteString[signers.length]; + signers[0] = new GenericBulletinBoardSignature(new ECDSASignature()); + signers[1] = new GenericBulletinBoardSignature(new ECDSASignature()); + + InputStream keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE); + char[] password = KEYFILE_PASSWORD1.toCharArray(); + + KeyStore.Builder keyStoreBuilder; + try { + keyStoreBuilder = signers[0].getPKCS12KeyStoreBuilder(keyStream, password); + + signers[0].loadSigningCertificate(keyStoreBuilder); + + signers[0].loadVerificationCertificates(getClass().getResourceAsStream(CERT1_PEM_EXAMPLE)); + + keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE3); + password = KEYFILE_PASSWORD3.toCharArray(); + + keyStoreBuilder = signers[1].getPKCS12KeyStoreBuilder(keyStream, password); + signers[1].loadSigningCertificate(keyStoreBuilder); + + signers[1].loadVerificationCertificates(getClass().getResourceAsStream(CERT3_PEM_EXAMPLE)); + + for (int i = 0 ; i < signers.length ; i++) { + signerIDs[i] = signers[i].getSignerID(); + } + + } catch (IOException e) { + System.err.println("Failed reading from signature file " + e.getMessage()); + fail("Failed reading from signature file " + e.getMessage()); + } catch (CertificateException e) { + System.err.println("Failed reading certificate " + e.getMessage()); + fail("Failed reading certificate " + e.getMessage()); + } catch (KeyStoreException e) { + System.err.println("Failed reading keystore " + e.getMessage()); + fail("Failed reading keystore " + e.getMessage()); + } catch (NoSuchAlgorithmException e) { + System.err.println("Couldn't find signing algorithm " + e.getMessage()); + fail("Couldn't find signing algorithm " + e.getMessage()); + } catch (UnrecoverableKeyException e) { + System.err.println("Couldn't find signing key " + e.getMessage()); + fail("Couldn't find signing key " + e.getMessage()); + } + + } + + /** + * Takes care of initializing the client and the test resources + */ + public void init(){ + + random = new Random(0); // We use insecure randomness in tests for repeatability + generator = new BulletinBoardMessageGenerator(random); + + thrown = new Vector<>(); + jobSemaphore = new Semaphore(0); + + } + + /** + * Closes the client and makes sure the test fails when an exception occurred in a separate thread + */ + + public void close() { + + if (thrown.size() > 0) { + assert false; + } + + } + + private class SubscriptionCallback implements FutureCallback>{ + + private int stage; + private final List> expectedMessages; + private final List messagesToPost; + private final BulletinBoardMessageComparator comparator; + + public SubscriptionCallback(List> expectedMessages, List messagesToPost) { + + this.expectedMessages = expectedMessages; + this.messagesToPost = messagesToPost; + this.stage = 0; + this.comparator = new BulletinBoardMessageComparator(); + + } + + @Override + public void onSuccess(List result) { + + if (stage >= expectedMessages.size()) + return; + + // Check for consistency + + List expectedMsgList = expectedMessages.get(stage); + + if (expectedMsgList.size() != result.size()){ + onFailure(new AssertionError("Received wrong number of messages")); + return; + } + + Iterator expectedMessageIterator = expectedMsgList.iterator(); + Iterator receivedMessageIterator = result.iterator(); + + while (expectedMessageIterator.hasNext()) { + if(comparator.compare(expectedMessageIterator.next(), receivedMessageIterator.next()) != 0){ + onFailure(new AssertionError("Received unexpected message")); + return; + } + } + + // Post new message + try { + if (stage < messagesToPost.size()) { + bulletinBoardClient.postMessage(messagesToPost.get(stage)); + } + } catch (CommunicationException e) { + onFailure(e); + return; + } + + stage++; + jobSemaphore.release(); + } + + @Override + public void onFailure(Throwable t) { + System.err.println(t.getCause() + " " + t.getMessage()); + thrown.add(t); + jobSemaphore.release(); + stage = expectedMessages.size(); + } + } + + public void subscriptionTest() throws SignatureException, CommunicationException { + + final String COMMON_TAG = "SUBSCRIPTION_TEST"; + + List tags = new LinkedList<>(); + tags.add(COMMON_TAG); + + BulletinBoardMessage msg1 = generator.generateRandomMessage(signers, Timestamp.newBuilder().setSeconds(1000).setNanos(900).build(), 10, 4, tags); + BulletinBoardMessage msg2 = generator.generateRandomMessage(signers, Timestamp.newBuilder().setSeconds(800).setNanos(300).build(), 10, 4); + BulletinBoardMessage msg3 = generator.generateRandomMessage(signers, Timestamp.newBuilder().setSeconds(2000).setNanos(0).build(), 10, 4, tags); + + MessageFilterList filterList = MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.TAG) + .setTag(COMMON_TAG) + .build()) + .build(); + + List> expectedMessages = new ArrayList<>(3); + expectedMessages.add(new LinkedList<>()); + expectedMessages.add(new LinkedList<>()); + expectedMessages.add(new LinkedList<>()); + expectedMessages.get(0).add(msg1); + expectedMessages.get(2).add(msg3); + + List messagesToPost = new ArrayList<>(2); + messagesToPost.add(msg2); + messagesToPost.add(msg3); + + bulletinBoardClient.postMessage(msg1); + bulletinBoardClient.subscribe(filterList, new SubscriptionCallback(expectedMessages, messagesToPost)); + + try { + jobSemaphore.acquire(3); + } catch (InterruptedException e) { + System.err.println(e.getCause() + " " + e.getMessage()); + } + + } + +} diff --git a/bulletin-board-client/src/test/java/meerkat/bulletinboard/LocalBulletinBoardClientTest.java b/bulletin-board-client/src/test/java/meerkat/bulletinboard/LocalBulletinBoardClientTest.java new file mode 100644 index 0000000..d33d5ba --- /dev/null +++ b/bulletin-board-client/src/test/java/meerkat/bulletinboard/LocalBulletinBoardClientTest.java @@ -0,0 +1,90 @@ +package meerkat.bulletinboard; + +import meerkat.bulletinboard.sqlserver.*; +import meerkat.comm.CommunicationException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.security.SignatureException; + +/** + * Created by Arbel Deutsch Peled on 05-Dec-15. + */ +public class LocalBulletinBoardClientTest { + + private static final int THREAD_NUM = 3; + private static final String DB_NAME = "TestDB"; + + private static final int SUBSRCIPTION_DELAY = 3000; + + // Testers + private GenericBulletinBoardClientTester clientTest; + private GenericSubscriptionClientTester subscriptionTester; + + public LocalBulletinBoardClientTest() throws CommunicationException { + + H2QueryProvider queryProvider = new H2QueryProvider(DB_NAME); + + DeletableBulletinBoardServer server = new BulletinBoardSQLServer(queryProvider); + server.init(); + + LocalBulletinBoardClient client = new LocalBulletinBoardClient(server, THREAD_NUM, SUBSRCIPTION_DELAY); + subscriptionTester = new GenericSubscriptionClientTester(client); + clientTest = new GenericBulletinBoardClientTester(client, 98354); + + } + + // Test methods + + /** + * Takes care of initializing the client and the test resources + */ + @Before + public void init(){ + + clientTest.init(); + + } + + /** + * Closes the client and makes sure the test fails when an exception occurred in a separate thread + */ + + @After + public void close() { + + clientTest.close(); + + } + + @Test + public void testPost() { + + clientTest.testPost(); + + } + + @Test + public void testBatchPost() throws CommunicationException, SignatureException, InterruptedException { + + clientTest.testBatchPost(); + } + + @Test + public void testCompleteBatchPost() throws CommunicationException, SignatureException, InterruptedException { + + clientTest.testCompleteBatchPost(); + + } + + @Test + public void testSubscription() throws SignatureException, CommunicationException { + + subscriptionTester.init(); + subscriptionTester.subscriptionTest(); + subscriptionTester.close(); + + } + +} diff --git a/bulletin-board-client/src/test/java/meerkat/bulletinboard/SingleServerBulletinBoardClientIntegrationTest.java b/bulletin-board-client/src/test/java/meerkat/bulletinboard/SingleServerBulletinBoardClientIntegrationTest.java new file mode 100644 index 0000000..7c8d58b --- /dev/null +++ b/bulletin-board-client/src/test/java/meerkat/bulletinboard/SingleServerBulletinBoardClientIntegrationTest.java @@ -0,0 +1,104 @@ +package meerkat.bulletinboard; + +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import meerkat.comm.CommunicationException; +import meerkat.protobuf.Voting.BulletinBoardClientParams; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.security.SignatureException; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Executors; + +/** + * Created by Arbel Deutsch Peled on 05-Dec-15. + */ +public class SingleServerBulletinBoardClientIntegrationTest { + + // Server data + + private static final String PROP_GETTY_URL = "gretty.httpBaseURI"; + private static final String DEFAULT_BASE_URL = "http://localhost:8081"; + private static final String BASE_URL = System.getProperty(PROP_GETTY_URL, DEFAULT_BASE_URL); + + private static final int THREAD_NUM = 3; + private static final long FAIL_DELAY = 3000; + private static final long SUBSCRIPTION_INTERVAL = 500; + + // Testers + private GenericBulletinBoardClientTester clientTest; + private GenericSubscriptionClientTester subscriptionTester; + + public SingleServerBulletinBoardClientIntegrationTest(){ + + SingleServerBulletinBoardClient client = new SingleServerBulletinBoardClient(THREAD_NUM, FAIL_DELAY, SUBSCRIPTION_INTERVAL); + + List testDB = new LinkedList<>(); + testDB.add(BASE_URL); + + client.init(BulletinBoardClientParams.newBuilder() + .addAllBulletinBoardAddress(testDB) + .setMinRedundancy((float) 1.0) + .build()); + + clientTest = new GenericBulletinBoardClientTester(client, 981541); + subscriptionTester = new GenericSubscriptionClientTester(client); + + } + + // Test methods + + /** + * Takes care of initializing the client and the test resources + */ + @Before + public void init(){ + + clientTest.init(); + + } + + /** + * Closes the client and makes sure the test fails when an exception occurred in a separate thread + */ + + @After + public void close() { + + clientTest.close(); + + } + + @Test + public void testPost() { + + clientTest.testPost(); + + } + + @Test + public void testBatchPost() throws CommunicationException, SignatureException, InterruptedException { + + clientTest.testBatchPost(); + } + + @Test + public void testCompleteBatchPost() throws CommunicationException, SignatureException, InterruptedException { + + clientTest.testCompleteBatchPost(); + + } + + @Test + public void testSubscription() throws SignatureException, CommunicationException { + + subscriptionTester.init(); + subscriptionTester.subscriptionTest(); + subscriptionTester.close(); + + } + +} diff --git a/bulletin-board-client/src/test/java/meerkat/bulletinboard/ThreadedBulletinBoardClientIntegrationTest.java b/bulletin-board-client/src/test/java/meerkat/bulletinboard/ThreadedBulletinBoardClientIntegrationTest.java new file mode 100644 index 0000000..1187245 --- /dev/null +++ b/bulletin-board-client/src/test/java/meerkat/bulletinboard/ThreadedBulletinBoardClientIntegrationTest.java @@ -0,0 +1,89 @@ +package meerkat.bulletinboard; + +import meerkat.comm.CommunicationException; + +import meerkat.protobuf.Voting.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.security.SignatureException; +import java.util.LinkedList; +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 05-Dec-15. + */ +public class ThreadedBulletinBoardClientIntegrationTest { + + // Server data + + private static String PROP_GETTY_URL = "gretty.httpBaseURI"; + private static String DEFAULT_BASE_URL = "http://localhost:8081"; + private static String BASE_URL = System.getProperty(PROP_GETTY_URL, DEFAULT_BASE_URL); + + // Tester + private GenericBulletinBoardClientTester clientTest; + + public ThreadedBulletinBoardClientIntegrationTest(){ + + ThreadedBulletinBoardClient client = new ThreadedBulletinBoardClient(3,0,500); + + List testDB = new LinkedList<>(); + testDB.add(BASE_URL); + + client.init(BulletinBoardClientParams.newBuilder() + .addAllBulletinBoardAddress(testDB) + .setMinRedundancy((float) 1.0) + .build()); + + clientTest = new GenericBulletinBoardClientTester(client, 52351); + + } + + // Test methods + + /** + * Takes care of initializing the client and the test resources + */ + @Before + public void init(){ + + clientTest.init(); + + } + + /** + * Closes the client and makes sure the test fails when an exception occurred in a separate thread + */ + + @After + public void close() { + + clientTest.close(); + + } + + @Test + public void testPost() { + + clientTest.testPost(); + + } + + @Test + public void testBatchPost() throws CommunicationException, SignatureException, InterruptedException { + + clientTest.testBatchPost(); + + } + + @Test + public void testCompleteBatchPost() throws CommunicationException, SignatureException, InterruptedException { + + clientTest.testCompleteBatchPost(); + + } + +} diff --git a/bulletin-board-server/build.gradle b/bulletin-board-server/build.gradle index 0bc5452..1606ef8 100644 --- a/bulletin-board-server/build.gradle +++ b/bulletin-board-server/build.gradle @@ -48,9 +48,10 @@ dependencies { // JDBC connections compile 'org.springframework:spring-jdbc:4.2.+' - compile 'org.xerial:sqlite-jdbc:3.7.+' + compile 'org.xerial:sqlite-jdbc:3.8.+' compile 'mysql:mysql-connector-java:5.1.+' compile 'com.h2database:h2:1.0.+' + compile 'org.apache.commons:commons-dbcp2:2.0.+' // Servlets compile 'javax.servlet:javax.servlet-api:3.0.+' @@ -79,9 +80,30 @@ test { exclude '**/*IntegrationTest*' } +task myTest(type: Test) { + include '**/*MySQL*Test*' + outputs.upToDateWhen { false } +} + +task h2Test(type: Test) { + include '**/*H2*Test*' + outputs.upToDateWhen { false } +} + +task liteTest(type: Test) { + include '**/*SQLite*Test*' + outputs.upToDateWhen { false } +} + task dbTest(type: Test) { include '**/*H2*Test*' - include '**/*MySql*Test' + include '**/*MySQL*Test*' + include '**/*SQLite*Test*' + outputs.upToDateWhen { false } +} + +task manualIntegration(type: Test) { + include '**/*IntegrationTest*' } task integrationTest(type: Test) { @@ -237,5 +259,3 @@ publishing { } } - - diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java index 52bf42b..bf99259 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/BulletinBoardSQLServer.java @@ -1,32 +1,46 @@ package meerkat.bulletinboard.sqlserver; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.sql.*; import java.util.*; -import com.google.protobuf.ProtocolStringList; +import com.google.protobuf.*; + +import com.google.protobuf.Timestamp; +import meerkat.bulletinboard.*; +import meerkat.bulletinboard.sqlserver.mappers.*; -import meerkat.bulletinboard.BulletinBoardServer; -import meerkat.bulletinboard.sqlserver.mappers.EntryNumMapper; -import meerkat.bulletinboard.sqlserver.mappers.MessageMapper; -import meerkat.bulletinboard.sqlserver.mappers.SignatureMapper; import meerkat.comm.CommunicationException; + +import meerkat.comm.MessageInputStream; +import meerkat.comm.MessageOutputStream; +import meerkat.crypto.DigitalSignature; +import meerkat.crypto.concrete.SHA256Digest; + import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.protobuf.Comm; import meerkat.protobuf.Crypto.Signature; import meerkat.protobuf.Crypto.SignatureVerificationKey; -import meerkat.crypto.Digest; -import meerkat.crypto.concrete.SHA256Digest; + + +import static meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer.SQLQueryProvider.*; import javax.sql.DataSource; +import meerkat.util.BulletinBoardUtils; +import meerkat.util.TimestampComparator; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; + + /** * This is a generic SQL implementation of the BulletinBoardServer API. */ -public class BulletinBoardSQLServer implements BulletinBoardServer{ +public class BulletinBoardSQLServer implements DeletableBulletinBoardServer{ /** * This interface provides the required implementation-specific data to enable an access to an actual SQL server. @@ -40,24 +54,135 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ */ public static enum QueryType { - FIND_MSG_ID(new String[] {"MsgId"}), - INSERT_MSG(new String[] {"MsgId","Msg"}), - INSERT_NEW_TAG(new String[] {"Tag"}), - CONNECT_TAG(new String[] {"EntryNum","Tag"}), - ADD_SIGNATURE(new String[] {"EntryNum","SignerId","Signature"}), - GET_SIGNATURES(new String[] {"EntryNum"}), - GET_MESSAGES(new String[] {}); + FIND_MSG_ID( + new String[] {"MsgId"}, + new int[] {Types.BLOB} + ), + + FIND_TAG_ID( + new String[] {"Tag"}, + new int[] {Types.VARCHAR} + ), + + INSERT_MSG( + new String[] {"MsgId","TimeStamp","Msg"}, + new int[] {Types.BLOB, Types.TIMESTAMP, Types.BLOB} + ), + + DELETE_MSG_BY_ENTRY( + new String[] {"EntryNum"}, + new int[] {Types.INTEGER} + ), + + DELETE_MSG_BY_ID( + new String[] {"MsgId"}, + new int[] {Types.BLOB} + ), + + INSERT_NEW_TAG( + new String[] {"Tag"}, + new int[] {Types.VARCHAR} + ), + + CONNECT_TAG( + new String[] {"EntryNum","Tag"}, + new int[] {Types.INTEGER, Types.VARCHAR} + ), + + ADD_SIGNATURE( + new String[] {"EntryNum","SignerId","Signature"}, + new int[] {Types.INTEGER, Types.BLOB, Types.BLOB} + ), + + GET_SIGNATURES( + new String[] {"EntryNum"}, + new int[] {Types.INTEGER} + ), + + GET_MESSAGES( + new String[] {}, + new int[] {} + ), + + COUNT_MESSAGES( + new String[] {}, + new int[] {} + ), + + GET_MESSAGE_STUBS( + new String[] {}, + new int[] {} + ), + + GET_LAST_MESSAGE_ENTRY( + new String[] {}, + new int[] {} + ), + + CHECK_BATCH_LENGTH( + new String[] {"BatchId"}, + new int[] {Types.BLOB, Types.INTEGER} + ), + + CHECK_BATCH_OPEN( + new String[] {"BatchId"}, + new int[] {Types.BLOB, Types.INTEGER} + ), + + GET_BATCH_MESSAGE_DATA_BY_MSG_ID( + new String[] {"MsgId", "StartPosition"}, + new int[] {Types.BLOB, Types.INTEGER} + ), + + GET_BATCH_MESSAGE_DATA_BY_BATCH_ID( + new String[] {"BatchId", "StartPosition"}, + new int[] {Types.INTEGER, Types.INTEGER} + ), + + INSERT_BATCH_DATA( + new String[] {"BatchId", "SerialNum", "Data"}, + new int[] {Types.INTEGER, Types.INTEGER, Types.BLOB} + ), + + STORE_BATCH_TAGS( + new String[] {"Tags"}, + new int[] {Types.BLOB} + ), + + GET_BATCH_TAGS( + new String[] {"BatchId"}, + new int[] {Types.BLOB, Types.INTEGER} + ), + + ADD_ENTRY_NUM_TO_BATCH( + new String[] {"BatchId", "EntryNum"}, + new int[] {Types.BLOB, Types.INTEGER, Types.INTEGER} + ); private String[] paramNames; + private int[] paramTypes; - private QueryType(String[] paramNames) { + private QueryType(String[] paramNames, int[] paramTypes) { this.paramNames = paramNames; + this.paramTypes = paramTypes; } public String[] getParamNames() { return paramNames; } + public String getParamName(int num) { + return paramNames[num]; + } + + public int[] getParamTypes() { + return paramTypes; + } + + public int getParamType(int num) { + return paramTypes[num]; + } + } /** @@ -69,7 +194,8 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ MSG_ID("MsgId", Types.BLOB), SIGNER_ID("SignerId", Types.BLOB), TAG("Tag", Types.VARCHAR), - LIMIT("Limit", Types.INTEGER); + LIMIT("Limit", Types.INTEGER), + TIMESTAMP("TimeStamp", Types.TIMESTAMP); private FilterTypeParam(String paramName, int paramType) { this.paramName = paramName; @@ -85,8 +211,9 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ case MSG_ID: return MSG_ID; - case EXACT_ENTRY: // Go through - case MAX_ENTRY: + case EXACT_ENTRY: // Go through + case MAX_ENTRY: // Go through + case MIN_ENTRY: return ENTRY_NUM; case SIGNER_ID: @@ -98,6 +225,10 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ case MAX_MESSAGES: return LIMIT; + case BEFORE_TIME: // Go through + case AFTER_TIME: + return TIMESTAMP; + default: return null; } @@ -152,16 +283,22 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ } + /** + * This method returns the value of the parameter specified in a message filter + * @param messageFilter is the filter + * @return the object parameter for the SQL query embedded in the filter (this depends on the filter type) + */ private Object getParam(MessageFilter messageFilter) { switch (messageFilter.getType()) { - case MSG_ID: // Go through + case MSG_ID: // Go through case SIGNER_ID: return messageFilter.getId().toByteArray(); - case EXACT_ENTRY: // Go through - case MAX_ENTRY: + case EXACT_ENTRY: // Go through + case MAX_ENTRY: // Go through + case MIN_ENTRY: return messageFilter.getEntry(); case TAG: @@ -170,7 +307,11 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ case MAX_MESSAGES: return messageFilter.getMaxMessages(); - default: + case BEFORE_TIME: // Go through + case AFTER_TIME: + return BulletinBoardUtils.toSQLTimestamp(messageFilter.getTimestamp()); + + default: // Unsupported filter type return null; } @@ -193,7 +334,8 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ protected NamedParameterJdbcTemplate jdbcTemplate; - protected Digest digest; + protected BulletinBoardDigest digest; + protected DigitalSignature signer; protected List trusteeSignatureVerificationArray; protected int minTrusteeSignatures; @@ -216,8 +358,6 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ */ private void createSchema() throws SQLException { - final int TIMEOUT = 20; - for (String command : sqlQueryProvider.getSchemaCreationCommands()) { jdbcTemplate.update(command,(Map) null); } @@ -228,10 +368,10 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ * This method initializes the signatures, connects to the DB and creates the schema (if required). */ @Override - public void init(String meerkatDB) throws CommunicationException { + public void init() throws CommunicationException { // TODO write signature reading part. - digest = new SHA256Digest(); + digest = new GenericBulletinBoardDigest(new SHA256Digest()); jdbcTemplate = new NamedParameterJdbcTemplate(sqlQueryProvider.getDataSource()); @@ -264,12 +404,12 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ String sql; - sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.INSERT_NEW_TAG); + sql = sqlQueryProvider.getSQLString(QueryType.INSERT_NEW_TAG); Map namedParameters[] = new HashMap[tags.length]; for (int i = 0 ; i < tags.length ; i++){ namedParameters[i] = new HashMap(); - namedParameters[i].put("Tag", tags[i]); + namedParameters[i].put(QueryType.INSERT_NEW_TAG.getParamName(0), tags[i]); } jdbcTemplate.batchUpdate(sql, namedParameters); @@ -277,21 +417,31 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ } /** - * This procedure is used to convert a boolean to a BoolMsg. + * This procedure is used to convert a boolean to a BoolValue. * @param b is the boolean to convert. * @return a ProtoBuf message with boolean payload. */ - private BoolMsg boolToBoolMsg(boolean b){ - return BoolMsg.newBuilder() + private BoolValue boolToBoolValue(boolean b){ + return BoolValue.newBuilder() .setValue(b) .build(); } - @Override - public BoolMsg postMessage(BulletinBoardMessage msg) throws CommunicationException { - - if (!verifyMessage(msg)) { - return boolToBoolMsg(false); + + /** + * This method posts a messages to the server + * @param msg is the message to post + * @param precalculatedMsgID is an optional precalculated message ID + * It is used when the message is the stub of a batch message + * In this case the validity of the signature is not checked either + * @return -1 if the message is not verified + * The entry number of the message if the message is posted + * @throws CommunicationException + */ + private long postMessage(BulletinBoardMessage msg, byte[] precalculatedMsgID) throws CommunicationException{ + + if (precalculatedMsgID != null && !verifyMessage(msg)) { + return -1; } String sql; @@ -299,28 +449,32 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ byte[] msgID; long entryNum; - + ProtocolStringList tagList; String[] tags; - + List signatureList; Signature[] signatures; - - // Calculate message ID (depending only on the the unsigned message) - - digest.reset(); - digest.update(msg.getMsg()); - - msgID = digest.digest(); - + + + if (precalculatedMsgID != null){ + msgID = precalculatedMsgID; + } else{ + // Calculate message ID (depending only on the the unsigned message) + digest.reset(); + digest.update(msg.getMsg()); + + msgID = digest.digest(); + } + // Add message to table if needed and store entry number of message. - - sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.FIND_MSG_ID); + sql = sqlQueryProvider.getSQLString(QueryType.FIND_MSG_ID); Map namedParameters = new HashMap(); - namedParameters.put("MsgId",msgID); - List entryNums = jdbcTemplate.query(sql, new MapSqlParameterSource(namedParameters), new EntryNumMapper()); + namedParameters.put(QueryType.FIND_MSG_ID.getParamName(0),msgID); + + List entryNums = jdbcTemplate.query(sql, namedParameters, new LongMapper()); if (entryNums.size() > 0){ @@ -328,99 +482,157 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ } else{ - sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.INSERT_MSG); - namedParameters.put("Msg", msg.getMsg().toByteArray()); + sql = sqlQueryProvider.getSQLString(QueryType.INSERT_MSG); + + namedParameters.put(QueryType.INSERT_MSG.getParamName(1), BulletinBoardUtils.toSQLTimestamp(msg.getMsg().getTimestamp())); + namedParameters.put(QueryType.INSERT_MSG.getParamName(2), msg.getMsg().toByteArray()); KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(sql,new MapSqlParameterSource(namedParameters),keyHolder); + jdbcTemplate.update(sql, new MapSqlParameterSource(namedParameters), keyHolder); entryNum = keyHolder.getKey().longValue(); } - + // Retrieve tags and store new ones in tag table. - + try { - - tagList = msg.getMsg().getTagList(); - tags = new String[tagList.size()]; - tags = tagList.toArray(tags); - - insertNewTags(tags); - + + tagList = msg.getMsg().getTagList(); + tags = new String[tagList.size()]; + tags = tagList.toArray(tags); + + insertNewTags(tags); + } catch (SQLException e) { throw new CommunicationException(e.getMessage()); } - + // Connect message to tags. - sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.CONNECT_TAG); + sql = sqlQueryProvider.getSQLString(QueryType.CONNECT_TAG); namedParameterArray = new HashMap[tags.length]; for (int i = 0 ; i < tags.length ; i++) { namedParameterArray[i] = new HashMap(); - namedParameterArray[i].put("EntryNum", entryNum); - namedParameterArray[i].put("Tag", tags[i]); + namedParameterArray[i].put(QueryType.CONNECT_TAG.getParamName(0), entryNum); + namedParameterArray[i].put(QueryType.CONNECT_TAG.getParamName(1), tags[i]); } jdbcTemplate.batchUpdate(sql, namedParameterArray); - + // Retrieve signatures. - - signatureList = msg.getSigList(); - signatures = new Signature[signatureList.size()]; - signatures = signatureList.toArray(signatures); - + + signatureList = msg.getSigList(); + signatures = new Signature[signatureList.size()]; + signatures = signatureList.toArray(signatures); + // Connect message to signatures. - sql = sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.ADD_SIGNATURE); + sql = sqlQueryProvider.getSQLString(QueryType.ADD_SIGNATURE); namedParameterArray = new HashMap[signatures.length]; for (int i = 0 ; i < signatures.length ; i++) { namedParameterArray[i] = new HashMap(); - namedParameterArray[i].put("EntryNum", entryNum); - namedParameterArray[i].put("SignerId", signatures[i].getSignerId().toByteArray()); - namedParameterArray[i].put("Signature", signatures[i].toByteArray()); + namedParameterArray[i].put(QueryType.ADD_SIGNATURE.getParamName(0), entryNum); + namedParameterArray[i].put(QueryType.ADD_SIGNATURE.getParamName(1), signatures[i].getSignerId().toByteArray()); + namedParameterArray[i].put(QueryType.ADD_SIGNATURE.getParamName(2), signatures[i].toByteArray()); } jdbcTemplate.batchUpdate(sql,namedParameterArray); - return boolToBoolMsg(true); + return entryNum; + + } + + private void checkConnection() throws CommunicationException { + if (jdbcTemplate == null) { + throw new CommunicationException("DB connection not initialized"); + } } @Override - public BulletinBoardMessageList readMessages(MessageFilterList filterList) throws CommunicationException { + public BoolValue postMessage(BulletinBoardMessage msg) throws CommunicationException { - BulletinBoardMessageList.Builder resultListBuilder = BulletinBoardMessageList.newBuilder(); + checkConnection(); - // SQL length is roughly 50 characters per filter + 50 for the query itself - StringBuilder sqlBuilder = new StringBuilder(50 * (filterList.getFilterCount() + 1)); + // Perform a post, calculate the message ID and check the signature for authenticity + if (postMessage(msg, null) != -1){ + return BoolValue.newBuilder().setValue(true).build(); // Message was posted + } - MapSqlParameterSource namedParameters; - int paramNum; + return BoolValue.newBuilder().setValue(false).build(); // Message was not posted - MessageMapper messageMapper = new MessageMapper(); - SignatureMapper signatureMapper = new SignatureMapper(); + } + + @Override + public BoolValue deleteMessage(MessageID msgID) throws CommunicationException { + + checkConnection(); + + String sql = sqlQueryProvider.getSQLString(QueryType.DELETE_MSG_BY_ID); + Map namedParameters = new HashMap(); + + namedParameters.put(QueryType.DELETE_MSG_BY_ID.getParamName(0),msgID); + + int affectedRows = jdbcTemplate.update(sql, namedParameters); + + //TODO: Log + + return BoolValue.newBuilder().setValue(affectedRows > 0).build(); + + } + + @Override + public BoolValue deleteMessage(long entryNum) throws CommunicationException { + + checkConnection(); + + String sql = sqlQueryProvider.getSQLString(QueryType.DELETE_MSG_BY_ENTRY); + Map namedParameters = new HashMap(); + + namedParameters.put(QueryType.DELETE_MSG_BY_ENTRY.getParamName(0),entryNum); + + int affectedRows = jdbcTemplate.update(sql, namedParameters); + + //TODO: Log + + return BoolValue.newBuilder().setValue(affectedRows > 0).build(); + + } + + + /** + * This is a container class for and SQL string builder and a MapSqlParameterSource to be used with it + */ + class SQLAndParameters { + + public StringBuilder sql; + public MapSqlParameterSource parameters; + + public SQLAndParameters(int numOfFilters) { + sql = new StringBuilder(50 * numOfFilters); + parameters = new MapSqlParameterSource(); + } + + } + + SQLAndParameters getSQLFromFilters(MessageFilterList filterList) { + + SQLAndParameters result = new SQLAndParameters(filterList.getFilterCount()); List filters = new ArrayList(filterList.getFilterList()); - boolean isFirstFilter = true; - Collections.sort(filters, new FilterTypeComparator()); - // Check if Tag/Signature tables are required for filtering purposes - - sqlBuilder.append(sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.GET_MESSAGES)); - // Add conditions - - namedParameters = new MapSqlParameterSource(); + boolean isFirstFilter = true; if (!filters.isEmpty()) { - sqlBuilder.append(" WHERE "); + result.sql.append(" WHERE "); - for (paramNum = 0 ; paramNum < filters.size() ; paramNum++) { + for (int paramNum = 0 ; paramNum < filters.size() ; paramNum++) { MessageFilter filter = filters.get(paramNum); @@ -428,15 +640,15 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ if (isFirstFilter) { isFirstFilter = false; } else { - sqlBuilder.append(" AND "); + result.sql.append(" AND "); } } - sqlBuilder.append(sqlQueryProvider.getCondition(filter.getType(), paramNum)); + result.sql.append(sqlQueryProvider.getCondition(filter.getType(), paramNum)); - SQLQueryProvider.FilterTypeParam filterTypeParam = SQLQueryProvider.FilterTypeParam.getFilterTypeParamName(filter.getType()); + FilterTypeParam filterTypeParam = FilterTypeParam.getFilterTypeParamName(filter.getType()); - namedParameters.addValue( + result.parameters.addValue( filterTypeParam.getParamName() + Integer.toString(paramNum), getParam(filter), filterTypeParam.getParamType(), @@ -446,39 +658,475 @@ public class BulletinBoardSQLServer implements BulletinBoardServer{ } + return result; + + } + + /** + * Private implementation of the message stub reader for returning result as a list + * @param filterList is a filter list that defines which messages the client is interested in + * @return the requested list of message stubs + */ + private List readMessageStubs(MessageFilterList filterList) { + + StringBuilder sqlBuilder = new StringBuilder(50 * (filterList.getFilterCount() + 1)); + + sqlBuilder.append(sqlQueryProvider.getSQLString(QueryType.GET_MESSAGE_STUBS)); + + // Get Conditions + + SQLAndParameters sqlAndParameters = getSQLFromFilters(filterList); + + sqlBuilder.append(sqlAndParameters.sql); + // Run query - List msgBuilders = jdbcTemplate.query(sqlBuilder.toString(), namedParameters, messageMapper); + return jdbcTemplate.query(sqlBuilder.toString(), sqlAndParameters.parameters, new MessageStubMapper()); - // Compile list of messages + } - for (BulletinBoardMessage.Builder msgBuilder : msgBuilders) { + /** + * Private implementation of the message reader for returning result as a list + * @param filterList is a filter list that defines which messages the client is interested in + * @return the requested list of messages + */ + private List readMessages(MessageFilterList filterList) throws CommunicationException { - // Retrieve signatures + try { - namedParameters = new MapSqlParameterSource(); - namedParameters.addValue("EntryNum", msgBuilder.getEntryNum()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - List signatures = jdbcTemplate.query( - sqlQueryProvider.getSQLString(SQLQueryProvider.QueryType.GET_SIGNATURES), - namedParameters, - signatureMapper); + readMessages(filterList, new MessageOutputStream(outputStream)); - // Append signatures - msgBuilder.addAllSig(signatures); + MessageInputStream inputStream = + MessageInputStream.MessageInputStreamFactory.createMessageInputStream(new ByteArrayInputStream( + outputStream.toByteArray()), + BulletinBoardMessage.class); - // Finalize message and add to message list. - - resultListBuilder.addMessage(msgBuilder.build()); + return inputStream.asList(); + } catch (Exception e) { + throw new CommunicationException(e.getCause() + " " + e.getMessage()); } - //Combine results and return. - return resultListBuilder.build(); + } + + + @Override + public void readMessages(MessageFilterList filterList, MessageOutputStream out) throws CommunicationException { + + checkConnection(); + + BulletinBoardMessageList.Builder resultListBuilder = BulletinBoardMessageList.newBuilder(); + + // SQL length is roughly 50 characters per filter + 50 for the query itself + StringBuilder sqlBuilder = new StringBuilder(50 * (filterList.getFilterCount() + 1)); + + // Check if Tag/Signature tables are required for filtering purposes + + sqlBuilder.append(sqlQueryProvider.getSQLString(QueryType.GET_MESSAGES)); + + // Get conditions + + SQLAndParameters sqlAndParameters = getSQLFromFilters(filterList); + sqlBuilder.append(sqlAndParameters.sql); + + // Run query and stream the output using a MessageCallbackHandler + + jdbcTemplate.query(sqlBuilder.toString(), sqlAndParameters.parameters, new MessageCallbackHandler(jdbcTemplate, sqlQueryProvider, out)); } @Override - public void close() {} + public Int32Value getMessageCount(MessageFilterList filterList) throws CommunicationException { + + checkConnection(); + + BulletinBoardMessageList.Builder resultListBuilder = BulletinBoardMessageList.newBuilder(); + + // SQL length is roughly 50 characters per filter + 50 for the query itself + StringBuilder sqlBuilder = new StringBuilder(50 * (filterList.getFilterCount() + 1)); + + // Check if Tag/Signature tables are required for filtering purposes + + sqlBuilder.append(sqlQueryProvider.getSQLString(QueryType.COUNT_MESSAGES)); + + // Get conditions + + SQLAndParameters sqlAndParameters = getSQLFromFilters(filterList); + sqlBuilder.append(sqlAndParameters.sql); + + // Run query and stream the output using a MessageCallbackHandler + + List count = jdbcTemplate.query(sqlBuilder.toString(), sqlAndParameters.parameters, new LongMapper()); + return Int32Value.newBuilder().setValue(count.get(0).intValue()).build(); + + } + + + /** + * This method checks if a specified batch exists and is already closed + * @param msgID is the unique ID of the batch message + * @return TRUE if the batch is closed and FALSE if it is still open or doesn't exist at all + */ + private boolean isBatchClosed(MessageID msgID) throws CommunicationException { + + MessageFilterList filterList = MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.MSG_ID) + .setId(msgID.getID()) + .build()) + .build(); + + + List messages = readMessages(filterList); + + if (messages.size() <= 0){ + return false; + } + + return (messages.get(0).getMsg().getDataTypeCase() == UnsignedBulletinBoardMessage.DataTypeCase.MSGID); + + } + + /** + * This method checks if a specified batch exists and is still open + * @param batchId is the temporary batch ID + * @return TRUE if the batch is closed and FALSE if it is still open or doesn't exist at all + */ + private boolean isBatchOpen(long batchId) throws CommunicationException { + + String sql = sqlQueryProvider.getSQLString(QueryType.CHECK_BATCH_OPEN); + MapSqlParameterSource namedParameters = new MapSqlParameterSource(); + + namedParameters.addValue(QueryType.CHECK_BATCH_OPEN.getParamName(0),batchId); + + List result = jdbcTemplate.query(sql, namedParameters, new LongMapper()); + + return (result.size() > 0 && result.get(0) > 0); + + } + + @Override + public Int64Value beginBatch(BeginBatchMessage message) throws CommunicationException { + + checkConnection(); + + // Store tags + String sql = sqlQueryProvider.getSQLString(QueryType.STORE_BATCH_TAGS); + MapSqlParameterSource namedParameters = new MapSqlParameterSource(); + + namedParameters.addValue(QueryType.STORE_BATCH_TAGS.getParamName(0),message.toByteArray()); + + jdbcTemplate.update(sql,namedParameters); + + KeyHolder keyHolder = new GeneratedKeyHolder(); + jdbcTemplate.update(sql, namedParameters, keyHolder); + + long entryNum = keyHolder.getKey().longValue(); + + return Int64Value.newBuilder().setValue(entryNum).build(); + + } + + + @Override + public BoolValue postBatchMessage(BatchMessage batchMessage) throws CommunicationException{ + + checkConnection(); + + // Make sure batch is open + if (!isBatchOpen(batchMessage.getBatchId())) { + return BoolValue.newBuilder().setValue(false).build(); + } + + // Add data + String sql = sqlQueryProvider.getSQLString(QueryType.INSERT_BATCH_DATA); + MapSqlParameterSource namedParameters = new MapSqlParameterSource(); + + namedParameters.addValue(QueryType.INSERT_BATCH_DATA.getParamName(0),batchMessage.getBatchId()); + namedParameters.addValue(QueryType.INSERT_BATCH_DATA.getParamName(1),batchMessage.getSerialNum()); + namedParameters.addValue(QueryType.INSERT_BATCH_DATA.getParamName(2),batchMessage.getData().toByteArray()); + + jdbcTemplate.update(sql, namedParameters); + + return BoolValue.newBuilder().setValue(true).build(); + + } + + + @Override + public BoolValue closeBatch(CloseBatchMessage message) throws CommunicationException { + + checkConnection(); + + // Check batch size + + String sql = sqlQueryProvider.getSQLString(QueryType.CHECK_BATCH_LENGTH); + MapSqlParameterSource namedParameters = new MapSqlParameterSource(); + + + namedParameters.addValue(QueryType.CHECK_BATCH_LENGTH.getParamName(0),message.getBatchId()); + + List lengthResult = jdbcTemplate.query(sql, namedParameters, new LongMapper()); + + if (lengthResult.get(0) != message.getBatchLength()) { + return BoolValue.newBuilder().setValue(false).build(); + } + + // Get Tags and add them to BulletinBoardMessage + + sql = sqlQueryProvider.getSQLString(QueryType.GET_BATCH_TAGS); + namedParameters = new MapSqlParameterSource(); + + namedParameters.addValue(QueryType.GET_BATCH_TAGS.getParamName(0),message.getBatchId()); + + List beginBatchMessages = jdbcTemplate.query(sql, namedParameters, new BeginBatchMessageMapper()); + + if (beginBatchMessages == null || beginBatchMessages.size() <= 0 || beginBatchMessages.get(0) == null) { + return BoolValue.newBuilder().setValue(false).build(); + } + + UnsignedBulletinBoardMessage unsignedMessage = UnsignedBulletinBoardMessage.newBuilder() + .addAllTag(beginBatchMessages.get(0).getTagList()) + .setTimestamp(message.getTimestamp()) + .build(); + + // Digest the data + + digest.reset(); + digest.update(unsignedMessage); + + sql = sqlQueryProvider.getSQLString(QueryType.GET_BATCH_MESSAGE_DATA_BY_BATCH_ID); + namedParameters = new MapSqlParameterSource(); + + namedParameters.addValue(QueryType.GET_BATCH_MESSAGE_DATA_BY_BATCH_ID.getParamName(0),message.getBatchId()); + namedParameters.addValue(QueryType.GET_BATCH_MESSAGE_DATA_BY_BATCH_ID.getParamName(1),0); // Read from the beginning + jdbcTemplate.query(sql, namedParameters, new BatchDataDigestHandler(digest)); + + byte[] msgID = digest.digest(); + + //TODO: Signature verification + + // Create Bulletin Board message + BulletinBoardMessage bulletinBoardMessage = BulletinBoardMessage.newBuilder() + .setMsg(UnsignedBulletinBoardMessage.newBuilder() + .mergeFrom(unsignedMessage) + .setMsgId(ByteString.copyFrom(msgID))) + .addAllSig(message.getSigList()) + .build(); + + // Post message with pre-calculated ID and without checking signature validity + long entryNum = postMessage(bulletinBoardMessage, msgID); + + // Add entry num to tag data table + sql = sqlQueryProvider.getSQLString(QueryType.ADD_ENTRY_NUM_TO_BATCH); + namedParameters = new MapSqlParameterSource(); + + namedParameters.addValue(QueryType.ADD_ENTRY_NUM_TO_BATCH.getParamName(0), message.getBatchId()); + namedParameters.addValue(QueryType.ADD_ENTRY_NUM_TO_BATCH.getParamName(1), entryNum); + + jdbcTemplate.update(sql, namedParameters); + + // Return TRUE + + return BoolValue.newBuilder().setValue(true).build(); + + } + + + @Override + public void readBatch(BatchQuery batchQuery, MessageOutputStream out) throws CommunicationException, IllegalArgumentException{ + + checkConnection(); + + // Check that batch is closed + if (!isBatchClosed(batchQuery.getMsgID())) { + throw new IllegalArgumentException("No such batch"); + } + + String sql = sqlQueryProvider.getSQLString(QueryType.GET_BATCH_MESSAGE_DATA_BY_MSG_ID); + MapSqlParameterSource namedParameters = new MapSqlParameterSource(); + + namedParameters.addValue(QueryType.GET_BATCH_MESSAGE_DATA_BY_MSG_ID.getParamName(0),batchQuery.getMsgID().getID().toByteArray()); + namedParameters.addValue(QueryType.GET_BATCH_MESSAGE_DATA_BY_MSG_ID.getParamName(1),batchQuery.getStartPosition()); + + jdbcTemplate.query(sql, namedParameters, new BatchDataCallbackHandler(out)); + + } + + /** + * Finds the entry number of the last entry in the database + * @return the entry number, or -1 if no entries are found + */ + protected long getLastMessageEntry() { + + String sql = sqlQueryProvider.getSQLString(QueryType.GET_LAST_MESSAGE_ENTRY); + + List resultList = jdbcTemplate.query(sql, new LongMapper()); + + if (resultList.size() <= 0){ + return -1; + } + + return resultList.get(0); + + } + + @Override + public SyncQuery generateSyncQuery(GenerateSyncQueryParams generateSyncQueryParams) throws CommunicationException{ + + checkConnection(); + + if (generateSyncQueryParams == null + || !generateSyncQueryParams.hasFilterList() + || generateSyncQueryParams.getFilterList().getFilterCount() <= 0 + || generateSyncQueryParams.getBreakpointListCount() <= 0){ + + return SyncQuery.getDefaultInstance(); + + } + + List messages = readMessageStubs(generateSyncQueryParams.getFilterList()); + + if (messages.size() <= 0){ + return SyncQuery.newBuilder().build(); + } + + SyncQuery.Builder resultBuilder = SyncQuery.newBuilder(); + + Iterator messageIterator = messages.iterator(); + Iterator breakpointIterator = generateSyncQueryParams.getBreakpointListList().iterator(); + + Checksum checksum = new SimpleChecksum(); + checksum.setDigest(new SHA256Digest()); + + Timestamp lastTimestamp = Timestamp.getDefaultInstance(); + BulletinBoardMessage message = messageIterator.next(); + long currentMessageNum = 1; + + boolean checksumChanged = true; + + while (breakpointIterator.hasNext()){ + + Float breakpoint = breakpointIterator.next(); + + // Continue while breakpoint not reached, or it has been reached but no new timestamp has been encountered since + while ( messageIterator.hasNext() + && ((float) currentMessageNum / (float) messages.size() <= breakpoint) + || ((float) currentMessageNum / (float) messages.size() > breakpoint + && lastTimestamp.equals(message.getMsg().getTimestamp()))){ + + checksumChanged = true; + + checksum.update(message.getMsg().getMsgId()); + + lastTimestamp = message.getMsg().getTimestamp(); + message = messageIterator.next(); + + } + + if (checksumChanged) { + + checksum.update(message.getMsg().getData()); + resultBuilder.addQuery(SingleSyncQuery.newBuilder() + .setTimeOfSync(message.getMsg().getTimestamp()) + .setChecksum(checksum.getChecksum()) + .build()); + + } + + checksumChanged = false; + + } + + return resultBuilder.build(); + + } + + + /** + * Searches for the latest time of sync of the DB relative to a given query and returns the metadata needed to complete the sync + * The checksum up to (and including) each given timestamp is calculated using an instance of SimpleChecksum + * @param syncQuery contains a succinct representation of states to compare against + * @return the current last entry num and latest time of sync if there is one; -1 as last entry and empty timestamp otherwise + * @throws CommunicationException + */ + @Override + public SyncQueryResponse querySync(SyncQuery syncQuery) throws CommunicationException { + + checkConnection(); + + if (syncQuery == null){ + return SyncQueryResponse.newBuilder() + .setLastEntryNum(-1) + .setLastTimeOfSync(com.google.protobuf.Timestamp.getDefaultInstance()) + .build(); + } + + com.google.protobuf.Timestamp lastTimeOfSync = null; + + TimestampComparator timestampComparator = new TimestampComparator(); + + long lastEntryNum = getLastMessageEntry(); + + Iterator queryIterator = syncQuery.getQueryList().iterator(); + + SingleSyncQuery currentQuery = queryIterator.next(); + + List messageStubs = readMessageStubs(syncQuery.getFilterList()); + + Checksum checksum = new SimpleChecksum(); + + for (BulletinBoardMessage message : messageStubs){ + + // Check for end of current query + if (timestampComparator.compare(message.getMsg().getTimestamp(), currentQuery.getTimeOfSync()) > 0){ + + if (checksum.getChecksum() == currentQuery.getChecksum()){ + lastTimeOfSync = currentQuery.getTimeOfSync(); + } else { + break; + } + + if (queryIterator.hasNext()){ + currentQuery = queryIterator.next(); + } else{ + break; + } + + } + + // Advance checksum + + ByteString messageID = message.getMsg().getMsgId(); // The data field contains the message ID + + checksum.update(messageID); + + } + + if (checksum.getChecksum() == currentQuery.getChecksum()){ + lastTimeOfSync = currentQuery.getTimeOfSync(); + } + + if (lastTimeOfSync == null){ + return SyncQueryResponse.newBuilder() + .setLastEntryNum(-1) + .setLastTimeOfSync(com.google.protobuf.Timestamp.getDefaultInstance()) + .build(); + } else{ + return SyncQueryResponse.newBuilder() + .setLastEntryNum(lastEntryNum) + .setLastTimeOfSync(lastTimeOfSync) + .build(); + } + + } + + + @Override + public void close() { + jdbcTemplate = null; + } } diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/H2QueryProvider.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/H2QueryProvider.java index fa2b146..872e226 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/H2QueryProvider.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/H2QueryProvider.java @@ -1,12 +1,10 @@ package meerkat.bulletinboard.sqlserver; import meerkat.protobuf.BulletinBoardAPI.FilterType; -import org.h2.jdbcx.JdbcDataSource; -import javax.naming.Context; -import javax.naming.InitialContext; +import org.apache.commons.dbcp2.BasicDataSource; -import javax.naming.NamingException; import javax.sql.DataSource; +import java.text.MessageFormat; import java.util.LinkedList; import java.util.List; @@ -28,32 +26,123 @@ public class H2QueryProvider implements BulletinBoardSQLServer.SQLQueryProvider switch(queryType) { case ADD_SIGNATURE: - return "INSERT INTO SignatureTable (EntryNum, SignerId, Signature)" - + " SELECT DISTINCT :EntryNum AS Entry, :SignerId AS Id, :Signature AS Sig FROM UtilityTable AS Temp" - + " WHERE NOT EXISTS" - + " (SELECT 1 FROM SignatureTable AS SubTable WHERE SubTable.SignerId = :SignerId AND SubTable.EntryNum = :EntryNum)"; + return MessageFormat.format( + "INSERT INTO SignatureTable (EntryNum, SignerId, Signature)" + + " SELECT DISTINCT :{0} AS Entry, :{1} AS Id, :{2} AS Sig FROM UtilityTable AS Temp" + + " WHERE NOT EXISTS" + + " (SELECT 1 FROM SignatureTable AS SubTable WHERE SubTable.EntryNum = :{0} AND SubTable.SignerId = :{1})", + QueryType.ADD_SIGNATURE.getParamName(0), + QueryType.ADD_SIGNATURE.getParamName(1), + QueryType.ADD_SIGNATURE.getParamName(2)); case CONNECT_TAG: - return "INSERT INTO MsgTagTable (TagId, EntryNum)" - + " SELECT DISTINCT TagTable.TagId, :EntryNum AS NewEntry FROM TagTable WHERE Tag = :Tag" - + " AND NOT EXISTS (SELECT 1 FROM MsgTagTable AS SubTable WHERE SubTable.TagId = TagTable.TagId" - + " AND SubTable.EntryNum = :EntryNum)"; + return MessageFormat.format( + "INSERT INTO MsgTagTable (TagId, EntryNum)" + + " SELECT DISTINCT TagTable.TagId, :{0} AS NewEntry FROM TagTable WHERE Tag = :{1}" + + " AND NOT EXISTS (SELECT 1 FROM MsgTagTable AS SubTable WHERE SubTable.TagId = TagTable.TagId" + + " AND SubTable.EntryNum = :{0})", + QueryType.CONNECT_TAG.getParamName(0), + QueryType.CONNECT_TAG.getParamName(1)); case FIND_MSG_ID: - return "SELECT EntryNum From MsgTable WHERE MsgId = :MsgId"; + return MessageFormat.format( + "SELECT EntryNum From MsgTable WHERE MsgId = :{0}", + QueryType.FIND_MSG_ID.getParamName(0)); + + case FIND_TAG_ID: + return MessageFormat.format( + "SELECT TagId FROM TagTable WHERE Tag = :{0}", + QueryType.FIND_TAG_ID.getParamName(0)); case GET_MESSAGES: return "SELECT MsgTable.EntryNum, MsgTable.Msg FROM MsgTable"; + case COUNT_MESSAGES: + return "SELECT COUNT(MsgTable.EntryNum) FROM MsgTable"; + + case GET_MESSAGE_STUBS: + return "SELECT MsgTable.EntryNum, MsgTable.MsgId, MsgTable.ExactTime FROM MsgTable"; + case GET_SIGNATURES: - return "SELECT Signature FROM SignatureTable WHERE EntryNum = :EntryNum"; + return MessageFormat.format( + "SELECT Signature FROM SignatureTable WHERE EntryNum = :{0}", + QueryType.GET_SIGNATURES.getParamName(0)); case INSERT_MSG: - return "INSERT INTO MsgTable (MsgId, Msg) VALUES(:MsgId,:Msg)"; + return MessageFormat.format( + "INSERT INTO MsgTable (MsgId, ExactTime, Msg) VALUES(:{0}, :{1}, :{2})", + QueryType.INSERT_MSG.getParamName(0), + QueryType.INSERT_MSG.getParamName(1), + QueryType.INSERT_MSG.getParamName(2)); + + case DELETE_MSG_BY_ENTRY: + return MessageFormat.format( + "DELETE FROM MsgTable WHERE EntryNum = :{0}", + QueryType.DELETE_MSG_BY_ENTRY.getParamName(0)); + + case DELETE_MSG_BY_ID: + return MessageFormat.format( + "DELETE FROM MsgTable WHERE MsgId = :{0}", + QueryType.DELETE_MSG_BY_ID.getParamName(0)); case INSERT_NEW_TAG: - return "INSERT INTO TagTable(Tag) SELECT DISTINCT :Tag AS NewTag FROM UtilityTable WHERE" - + " NOT EXISTS (SELECT 1 FROM TagTable AS SubTable WHERE SubTable.Tag = :Tag)"; + return MessageFormat.format( + "INSERT INTO TagTable(Tag) SELECT DISTINCT :Tag AS NewTag FROM UtilityTable WHERE" + + " NOT EXISTS (SELECT 1 FROM TagTable AS SubTable WHERE SubTable.Tag = :{0})", + QueryType.INSERT_NEW_TAG.getParamName(0)); + + case GET_LAST_MESSAGE_ENTRY: + return "SELECT MAX(MsgTable.EntryNum) FROM MsgTable"; + + case GET_BATCH_MESSAGE_DATA_BY_MSG_ID: + return MessageFormat.format( + "SELECT Data FROM BatchTable" + + " INNER JOIN MsgTable ON MsgTable.EntryNum = BatchTable.EntryNum" + + " WHERE MsgTable.MsgId = :{0} AND BatchTable.SerialNum >= :{1}" + + " ORDER BY BatchTable.SerialNum ASC", + QueryType.GET_BATCH_MESSAGE_DATA_BY_MSG_ID.getParamName(0), + QueryType.GET_BATCH_MESSAGE_DATA_BY_MSG_ID.getParamName(1)); + + case GET_BATCH_MESSAGE_DATA_BY_BATCH_ID: + return MessageFormat.format( + "SELECT Data FROM BatchTable" + + " WHERE BatchId = :{0} AND SerialNum >= :{1}" + + " ORDER BY BatchTable.SerialNum ASC", + QueryType.GET_BATCH_MESSAGE_DATA_BY_BATCH_ID.getParamName(0), + QueryType.GET_BATCH_MESSAGE_DATA_BY_BATCH_ID.getParamName(1)); + + case INSERT_BATCH_DATA: + return MessageFormat.format( + "INSERT INTO BatchTable (BatchId, SerialNum, Data) VALUES (:{0}, :{1}, :{2})", + QueryType.INSERT_BATCH_DATA.getParamName(0), + QueryType.INSERT_BATCH_DATA.getParamName(1), + QueryType.INSERT_BATCH_DATA.getParamName(2)); + + case CHECK_BATCH_LENGTH: + return MessageFormat.format( + "SELECT COUNT(Data) AS BatchLength FROM BatchTable WHERE BatchId = :{0}", + QueryType.CHECK_BATCH_LENGTH.getParamName(0)); + + case CHECK_BATCH_OPEN: + return MessageFormat.format( + "SELECT COUNT(BatchId) AS batchCount FROM BatchTagTable WHERE BatchId = :{0}", + QueryType.CHECK_BATCH_OPEN.getParamName(0)); + + case STORE_BATCH_TAGS: + return MessageFormat.format( + "INSERT INTO BatchTagTable (Tags) VALUES (:{0})", + QueryType.STORE_BATCH_TAGS.getParamName(0)); + + case GET_BATCH_TAGS: + return MessageFormat.format( + "SELECT Tags FROM BatchTagTable WHERE BatchId = :{0}", + QueryType.GET_BATCH_TAGS.getParamName(0)); + + case ADD_ENTRY_NUM_TO_BATCH: + return MessageFormat.format( + "UPDATE BatchTable SET EntryNum = :{1} WHERE BatchId = :{0}", + QueryType.ADD_ENTRY_NUM_TO_BATCH.getParamName(0), + QueryType.ADD_ENTRY_NUM_TO_BATCH.getParamName(1)); default: throw new IllegalArgumentException("Cannot serve a query of type " + queryType); @@ -71,10 +160,12 @@ public class H2QueryProvider implements BulletinBoardSQLServer.SQLQueryProvider return "MsgTable.EntryNum = :EntryNum" + serialString; case MAX_ENTRY: return "MsgTable.EntryNum <= :EntryNum" + serialString; + case MIN_ENTRY: + return "MsgTable.EntryNum >= :EntryNum" + serialString; case MAX_MESSAGES: return "LIMIT :Limit" + serialString; case MSG_ID: - return "MsgTable.MsgId = MsgId" + serialString; + return "MsgTable.MsgId = :MsgId" + serialString; case SIGNER_ID: return "EXISTS (SELECT 1 FROM SignatureTable" + " WHERE SignatureTable.SignerId = :SignerId" + serialString + " AND SignatureTable.EntryNum = MsgTable.EntryNum)"; @@ -82,6 +173,13 @@ public class H2QueryProvider implements BulletinBoardSQLServer.SQLQueryProvider return "EXISTS (SELECT 1 FROM TagTable" + " INNER JOIN MsgTagTable ON TagTable.TagId = MsgTagTable.TagId" + " WHERE TagTable.Tag = :Tag" + serialString + " AND MsgTagTable.EntryNum = MsgTable.EntryNum)"; + + case BEFORE_TIME: + return "MsgTable.ExactTime <= :TimeStamp" + serialString; + + case AFTER_TIME: + return "MsgTable.ExactTime >= :TimeStamp" + serialString; + default: throw new IllegalArgumentException("Cannot serve a filter of type " + filterType); } @@ -94,6 +192,7 @@ public class H2QueryProvider implements BulletinBoardSQLServer.SQLQueryProvider switch(filterType) { case EXACT_ENTRY: // Go through case MAX_ENTRY: // Go through + case MIN_ENTRY: // Go through case MAX_MESSAGES: return "INT"; @@ -104,6 +203,11 @@ public class H2QueryProvider implements BulletinBoardSQLServer.SQLQueryProvider case TAG: return "VARCHAR"; + case AFTER_TIME: // Go through + case BEFORE_TIME: + return "TIMESTAMP"; + + default: throw new IllegalArgumentException("Cannot serve a filter of type " + filterType); } @@ -113,10 +217,13 @@ public class H2QueryProvider implements BulletinBoardSQLServer.SQLQueryProvider @Override public DataSource getDataSource() { - JdbcDataSource dataSource = new JdbcDataSource(); - dataSource.setURL("jdbc:h2:~/" + dbName); + BasicDataSource dataSource = new BasicDataSource(); + + dataSource.setDriverClassName("org.h2.Driver"); + dataSource.setUrl("jdbc:h2:mem:" + dbName); return dataSource; + } @@ -124,20 +231,30 @@ public class H2QueryProvider implements BulletinBoardSQLServer.SQLQueryProvider public List getSchemaCreationCommands() { List list = new LinkedList(); - list.add("CREATE TABLE IF NOT EXISTS MsgTable (EntryNum INT NOT NULL AUTO_INCREMENT PRIMARY KEY, MsgId TINYBLOB UNIQUE, Msg BLOB)"); + list.add("CREATE TABLE IF NOT EXISTS MsgTable (EntryNum INT NOT NULL AUTO_INCREMENT PRIMARY KEY," + + " MsgId TINYBLOB UNIQUE, ExactTime TIMESTAMP, Msg BLOB)"); list.add("CREATE TABLE IF NOT EXISTS TagTable (TagId INT NOT NULL AUTO_INCREMENT PRIMARY KEY, Tag VARCHAR(50) UNIQUE)"); list.add("CREATE TABLE IF NOT EXISTS MsgTagTable (EntryNum INT, TagId INT," - + " FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum)," - + " FOREIGN KEY (TagId) REFERENCES TagTable(TagId)," + + " FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum) ON DELETE CASCADE," + + " FOREIGN KEY (TagId) REFERENCES TagTable(TagId) ON DELETE CASCADE," + " UNIQUE (EntryNum, TagID))"); list.add("CREATE TABLE IF NOT EXISTS SignatureTable (EntryNum INT, SignerId TINYBLOB, Signature TINYBLOB UNIQUE," - + " FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum))"); + + " FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum) ON DELETE CASCADE)"); list.add("CREATE INDEX IF NOT EXISTS SignerIndex ON SignatureTable(SignerId)"); - list.add("CREATE UNIQUE INDEX IF NOT EXISTS SignerIndex ON SignatureTable(SignerId, EntryNum)"); + list.add("CREATE UNIQUE INDEX IF NOT EXISTS SignatureIndex ON SignatureTable(SignerId, EntryNum)"); + + list.add("CREATE TABLE IF NOT EXISTS BatchTagTable (BatchId INT AUTO_INCREMENT PRIMARY KEY, Tags BLOB)"); + + list.add("CREATE TABLE IF NOT EXISTS BatchTable (BatchId INT, EntryNum INT, SerialNum INT, Data BLOB," + + " UNIQUE(BatchId, SerialNum)," + + " FOREIGN KEY (BatchId) REFERENCES BatchTagTable(BatchId) ON DELETE CASCADE)"); + list.add("CREATE INDEX IF NOT EXISTS BatchDataIndex ON BatchTable(EntryNum, SerialNum)"); + + // This is used to create a simple table with one entry. // It is used for implementing a workaround for the missing INSERT IGNORE syntax @@ -152,10 +269,20 @@ public class H2QueryProvider implements BulletinBoardSQLServer.SQLQueryProvider List list = new LinkedList(); list.add("DROP TABLE IF EXISTS UtilityTable"); - list.add("DROP INDEX IF EXISTS SignerIdIndex"); + + list.add("DROP INDEX IF EXISTS BatchDataIndex"); + list.add("DROP TABLE IF EXISTS BatchTable"); + + list.add("DROP INDEX IF EXISTS BatchTagIndex"); + list.add("DROP TABLE IF EXISTS BatchTagTable"); + list.add("DROP TABLE IF EXISTS MsgTagTable"); + + list.add("DROP INDEX IF EXISTS SignerIdIndex"); list.add("DROP TABLE IF EXISTS SignatureTable"); + list.add("DROP TABLE IF EXISTS TagTable"); + list.add("DROP TABLE IF EXISTS MsgTable"); return list; diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/MySQLQueryProvider.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/MySQLQueryProvider.java index c00c044..097095f 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/MySQLQueryProvider.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/MySQLQueryProvider.java @@ -1,10 +1,11 @@ package meerkat.bulletinboard.sqlserver; -import com.mysql.jdbc.jdbc2.optional.MysqlDataSource; import meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer.SQLQueryProvider; import meerkat.protobuf.BulletinBoardAPI.FilterType; +import org.apache.commons.dbcp2.BasicDataSource; import javax.sql.DataSource; +import java.text.MessageFormat; import java.util.LinkedList; import java.util.List; @@ -32,21 +33,120 @@ public class MySQLQueryProvider implements SQLQueryProvider { public String getSQLString(QueryType queryType) throws IllegalArgumentException{ switch(queryType) { + case ADD_SIGNATURE: - return "INSERT IGNORE INTO SignatureTable (EntryNum, SignerId, Signature) VALUES (:EntryNum, :SignerId, :Signature)"; + return MessageFormat.format( + "INSERT IGNORE INTO SignatureTable (EntryNum, SignerId, Signature) VALUES (:{0}, :{1}, :{2})", + QueryType.ADD_SIGNATURE.getParamName(0), + QueryType.ADD_SIGNATURE.getParamName(1), + QueryType.ADD_SIGNATURE.getParamName(2)); + case CONNECT_TAG: - return "INSERT IGNORE INTO MsgTagTable (TagId, EntryNum)" - + " SELECT TagTable.TagId, :EntryNum AS EntryNum FROM TagTable WHERE Tag = :Tag"; + return MessageFormat.format( + "INSERT IGNORE INTO MsgTagTable (TagId, EntryNum)" + + " SELECT TagTable.TagId, :{0} AS EntryNum FROM TagTable WHERE Tag = :{1}", + QueryType.CONNECT_TAG.getParamName(0), + QueryType.CONNECT_TAG.getParamName(1)); + case FIND_MSG_ID: - return "SELECT EntryNum From MsgTable WHERE MsgId = :MsgId"; + return MessageFormat.format( + "SELECT EntryNum From MsgTable WHERE MsgId = :{0}", + QueryType.FIND_MSG_ID.getParamName(0)); + + case FIND_TAG_ID: + return MessageFormat.format( + "SELECT TagId FROM TagTable WHERE Tag = :{0}", + QueryType.FIND_TAG_ID.getParamName(0)); + case GET_MESSAGES: return "SELECT MsgTable.EntryNum, MsgTable.Msg FROM MsgTable"; + + case COUNT_MESSAGES: + return "SELECT COUNT(MsgTable.EntryNum) FROM MsgTable"; + + case GET_MESSAGE_STUBS: + return "SELECT MsgTable.EntryNum, MsgTable.MsgId, MsgTable.ExactTime FROM MsgTable"; + case GET_SIGNATURES: - return "SELECT Signature FROM SignatureTable WHERE EntryNum = :EntryNum"; + return MessageFormat.format( + "SELECT Signature FROM SignatureTable WHERE EntryNum = :{0}", + QueryType.GET_SIGNATURES.getParamName(0)); + case INSERT_MSG: - return "INSERT INTO MsgTable (MsgId, Msg) VALUES(:MsgId, :Msg)"; + return MessageFormat.format( + "INSERT INTO MsgTable (MsgId, ExactTime, Msg) VALUES(:{0}, :{1}, :{2})", + QueryType.INSERT_MSG.getParamName(0), + QueryType.INSERT_MSG.getParamName(1), + QueryType.INSERT_MSG.getParamName(2)); + + case DELETE_MSG_BY_ENTRY: + return MessageFormat.format( + "DELETE IGNORE FROM MsgTable WHERE EntryNum = :{0}", + QueryType.DELETE_MSG_BY_ENTRY.getParamName(0)); + + case DELETE_MSG_BY_ID: + return MessageFormat.format( + "DELETE IGNORE FROM MsgTable WHERE MsgId = :{0}", + QueryType.DELETE_MSG_BY_ID.getParamName(0)); + case INSERT_NEW_TAG: - return "INSERT IGNORE INTO TagTable(Tag) VALUES (:Tag)"; + return MessageFormat.format( + "INSERT IGNORE INTO TagTable(Tag) VALUES (:{0})", + QueryType.INSERT_NEW_TAG.getParamName(0)); + + case GET_LAST_MESSAGE_ENTRY: + return "SELECT MAX(MsgTable.EntryNum) FROM MsgTable"; + + case GET_BATCH_MESSAGE_DATA_BY_MSG_ID: + return MessageFormat.format( + "SELECT Data FROM BatchTable" + + " INNER JOIN MsgTable ON MsgTable.EntryNum = BatchTable.EntryNum" + + " WHERE MsgTable.MsgId = :{0} AND BatchTable.SerialNum >= :{1}" + + " ORDER BY BatchTable.SerialNum ASC", + QueryType.GET_BATCH_MESSAGE_DATA_BY_MSG_ID.getParamName(0), + QueryType.GET_BATCH_MESSAGE_DATA_BY_MSG_ID.getParamName(1)); + + case GET_BATCH_MESSAGE_DATA_BY_BATCH_ID: + return MessageFormat.format( + "SELECT Data FROM BatchTable" + + " WHERE BatchId = :{0} AND SerialNum >= :{1}" + + " ORDER BY BatchTable.SerialNum ASC", + QueryType.GET_BATCH_MESSAGE_DATA_BY_BATCH_ID.getParamName(0), + QueryType.GET_BATCH_MESSAGE_DATA_BY_BATCH_ID.getParamName(1)); + + case INSERT_BATCH_DATA: + return MessageFormat.format( + "INSERT INTO BatchTable (BatchId, SerialNum, Data) VALUES (:{0}, :{1}, :{2})", + QueryType.INSERT_BATCH_DATA.getParamName(0), + QueryType.INSERT_BATCH_DATA.getParamName(1), + QueryType.INSERT_BATCH_DATA.getParamName(2)); + + case CHECK_BATCH_LENGTH: + return MessageFormat.format( + "SELECT COUNT(Data) AS BatchLength FROM BatchTable WHERE BatchId = :{0}", + QueryType.CHECK_BATCH_LENGTH.getParamName(0)); + + case CHECK_BATCH_OPEN: + return MessageFormat.format( + "SELECT COUNT(BatchId) AS batchCount FROM BatchTagTable WHERE BatchId = :{0}", + QueryType.CHECK_BATCH_OPEN.getParamName(0)); + + case STORE_BATCH_TAGS: + return MessageFormat.format( + "INSERT INTO BatchTagTable (Tags) VALUES (:{0})", + QueryType.STORE_BATCH_TAGS.getParamName(0)); + + case GET_BATCH_TAGS: + return MessageFormat.format( + "SELECT Tags FROM BatchTagTable WHERE BatchId = :{0}", + QueryType.GET_BATCH_TAGS.getParamName(0)); + + case ADD_ENTRY_NUM_TO_BATCH: + return MessageFormat.format( + "UPDATE BatchTable SET EntryNum = :{1} WHERE BatchId = :{0}", + QueryType.ADD_ENTRY_NUM_TO_BATCH.getParamName(0), + QueryType.ADD_ENTRY_NUM_TO_BATCH.getParamName(1)); + default: throw new IllegalArgumentException("Cannot serve a query of type " + queryType); } @@ -63,6 +163,8 @@ public class MySQLQueryProvider implements SQLQueryProvider { return "MsgTable.EntryNum = :EntryNum" + serialString; case MAX_ENTRY: return "MsgTable.EntryNum <= :EntryNum" + serialString; + case MIN_ENTRY: + return "MsgTable.EntryNum >= :EntryNum" + serialString; case MAX_MESSAGES: return "LIMIT :Limit" + serialString; case MSG_ID: @@ -74,6 +176,13 @@ public class MySQLQueryProvider implements SQLQueryProvider { return "EXISTS (SELECT 1 FROM TagTable" + " INNER JOIN MsgTagTable ON TagTable.TagId = MsgTagTable.TagId" + " WHERE TagTable.Tag = :Tag" + serialString + " AND MsgTagTable.EntryNum = MsgTable.EntryNum)"; + + case BEFORE_TIME: + return "MsgTable.ExactTime <= :TimeStamp"; + + case AFTER_TIME: + return "MsgTable.ExactTime >= :TimeStamp"; + default: throw new IllegalArgumentException("Cannot serve a filter of type " + filterType); } @@ -86,6 +195,7 @@ public class MySQLQueryProvider implements SQLQueryProvider { switch(filterType) { case EXACT_ENTRY: // Go through case MAX_ENTRY: // Go through + case MIN_ENTRY: // Go through case MAX_MESSAGES: return "INT"; @@ -96,6 +206,10 @@ public class MySQLQueryProvider implements SQLQueryProvider { case TAG: return "VARCHAR"; + case AFTER_TIME: // Go through + case BEFORE_TIME: + return "TIMESTAMP"; + default: throw new IllegalArgumentException("Cannot serve a filter of type " + filterType); } @@ -104,32 +218,44 @@ public class MySQLQueryProvider implements SQLQueryProvider { @Override public DataSource getDataSource() { - MysqlDataSource dataSource = new MysqlDataSource(); - dataSource.setServerName(dbAddress); - dataSource.setPort(dbPort); - dataSource.setDatabaseName(dbName); - dataSource.setUser(username); + BasicDataSource dataSource = new BasicDataSource(); + + dataSource.setDriverClassName("com.mysql.jdbc.Driver"); + dataSource.setUrl("jdbc:mysql://" + dbAddress + ":" + dbPort + "/" + dbName); + + dataSource.setUsername(username); dataSource.setPassword(password); return dataSource; + } @Override public List getSchemaCreationCommands() { List list = new LinkedList(); - list.add("CREATE TABLE IF NOT EXISTS MsgTable (EntryNum INT NOT NULL AUTO_INCREMENT PRIMARY KEY, MsgId TINYBLOB, Msg BLOB, UNIQUE(MsgId(50)))"); + list.add("CREATE TABLE IF NOT EXISTS MsgTable (EntryNum INT NOT NULL AUTO_INCREMENT PRIMARY KEY," + + " MsgId TINYBLOB, ExactTime TIMESTAMP, Msg BLOB, UNIQUE(MsgId(50)))"); list.add("CREATE TABLE IF NOT EXISTS TagTable (TagId INT NOT NULL AUTO_INCREMENT PRIMARY KEY, Tag VARCHAR(50), UNIQUE(Tag))"); list.add("CREATE TABLE IF NOT EXISTS MsgTagTable (EntryNum INT, TagId INT," - + " CONSTRAINT FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum)," - + " CONSTRAINT FOREIGN KEY (TagId) REFERENCES TagTable(TagId)," + + " CONSTRAINT FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum) ON DELETE CASCADE," + + " CONSTRAINT FOREIGN KEY (TagId) REFERENCES TagTable(TagId) ON DELETE CASCADE," + " CONSTRAINT UNIQUE (EntryNum, TagID))"); list.add("CREATE TABLE IF NOT EXISTS SignatureTable (EntryNum INT, SignerId TINYBLOB, Signature TINYBLOB," - + " INDEX(SignerId(32)), CONSTRAINT Uni UNIQUE(SignerId(32), EntryNum), CONSTRAINT FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum))"); + + " INDEX(SignerId(32)), CONSTRAINT Unique_Signature UNIQUE(SignerId(32), EntryNum)," + + " CONSTRAINT FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum) ON DELETE CASCADE)"); + + list.add("CREATE TABLE IF NOT EXISTS BatchTagTable (BatchId INT AUTO_INCREMENT PRIMARY KEY, Tags BLOB)"); + + list.add("CREATE TABLE IF NOT EXISTS BatchTable (BatchId INT, EntryNum INT, SerialNum INT, Data BLOB," + + " CONSTRAINT UNIQUE(BatchId, SerialNum)," + + " CONSTRAINT FOREIGN KEY (BatchId) REFERENCES BatchTagTable(BatchId) ON DELETE CASCADE)"); + + return list; } @@ -138,6 +264,8 @@ public class MySQLQueryProvider implements SQLQueryProvider { public List getSchemaDeletionCommands() { List list = new LinkedList(); + list.add("DROP TABLE IF EXISTS BatchTable"); + list.add("DROP TABLE IF EXISTS BatchTagTable"); list.add("DROP TABLE IF EXISTS MsgTagTable"); list.add("DROP TABLE IF EXISTS SignatureTable"); list.add("DROP TABLE IF EXISTS TagTable"); @@ -145,4 +273,4 @@ public class MySQLQueryProvider implements SQLQueryProvider { return list; } -} +} \ No newline at end of file diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteQueryProvider.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteQueryProvider.java index 945ae47..9f68955 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteQueryProvider.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/SQLiteQueryProvider.java @@ -1,9 +1,11 @@ package meerkat.bulletinboard.sqlserver; import meerkat.protobuf.BulletinBoardAPI.*; +import org.apache.commons.dbcp2.BasicDataSource; import org.sqlite.SQLiteDataSource; import javax.sql.DataSource; +import java.text.MessageFormat; import java.util.LinkedList; import java.util.List; @@ -25,19 +27,90 @@ public class SQLiteQueryProvider implements BulletinBoardSQLServer.SQLQueryProvi switch(queryType) { case ADD_SIGNATURE: return "INSERT OR IGNORE INTO SignatureTable (EntryNum, SignerId, Signature) VALUES (:EntryNum,:SignerId,:Signature)"; + case CONNECT_TAG: return "INSERT OR IGNORE INTO MsgTagTable (TagId, EntryNum)" + " SELECT TagTable.TagId, :EntryNum AS EntryNum FROM TagTable WHERE Tag = :Tag"; + case FIND_MSG_ID: return "SELECT EntryNum From MsgTable WHERE MsgId = :MsgId"; + + case FIND_TAG_ID: + return MessageFormat.format( + "SELECT TagId FROM TagTable WHERE Tag = :{0}", + QueryType.FIND_TAG_ID.getParamName(0)); + case GET_MESSAGES: return "SELECT MsgTable.EntryNum, MsgTable.Msg FROM MsgTable"; + + case COUNT_MESSAGES: + return "SELECT COUNT(MsgTable.EntryNum) FROM MsgTable"; + + case GET_MESSAGE_STUBS: + return "SELECT MsgTable.EntryNum, MsgTable.MsgId, MsgTable.ExactTime FROM MsgTable"; + case GET_SIGNATURES: return "SELECT Signature FROM SignatureTable WHERE EntryNum = :EntryNum"; + case INSERT_MSG: return "INSERT INTO MsgTable (MsgId, Msg) VALUES(:MsgId,:Msg)"; + case INSERT_NEW_TAG: return "INSERT OR IGNORE INTO TagTable(Tag) VALUES (:Tag)"; + + case GET_LAST_MESSAGE_ENTRY: + return "SELECT MAX(MsgTable.EntryNum) FROM MsgTable"; + + case GET_BATCH_MESSAGE_DATA_BY_MSG_ID: + return MessageFormat.format( + "SELECT Data FROM BatchTable" + + " INNER JOIN MsgTable ON MsgTable.EntryNum = BatchTable.EntryNum" + + " WHERE MsgTable.MsgId = :{0} AND BatchTable.SerialNum >= :{1}" + + " ORDER BY BatchTable.SerialNum ASC", + QueryType.GET_BATCH_MESSAGE_DATA_BY_MSG_ID.getParamName(0), + QueryType.GET_BATCH_MESSAGE_DATA_BY_MSG_ID.getParamName(1)); + + case GET_BATCH_MESSAGE_DATA_BY_BATCH_ID: + return MessageFormat.format( + "SELECT Data FROM BatchTable" + + " WHERE BatchId = :{0} AND SerialNum >= :{1}" + + " ORDER BY BatchTable.SerialNum ASC", + QueryType.GET_BATCH_MESSAGE_DATA_BY_BATCH_ID.getParamName(0), + QueryType.GET_BATCH_MESSAGE_DATA_BY_BATCH_ID.getParamName(1)); + + case INSERT_BATCH_DATA: + return MessageFormat.format( + "INSERT INTO BatchTable (BatchId, SerialNum, Data) VALUES (:{0}, :{1}, :{2})", + QueryType.INSERT_BATCH_DATA.getParamName(0), + QueryType.INSERT_BATCH_DATA.getParamName(1), + QueryType.INSERT_BATCH_DATA.getParamName(2)); + + case CHECK_BATCH_LENGTH: + return MessageFormat.format( + "SELECT COUNT(Data) AS BatchLength FROM BatchTable WHERE BatchId = :{0}", + QueryType.CHECK_BATCH_LENGTH.getParamName(0)); + + case CHECK_BATCH_OPEN: + return MessageFormat.format( + "SELECT COUNT(BatchId) AS batchCount FROM BatchTagTable WHERE BatchId = :{0}", + QueryType.CHECK_BATCH_OPEN.getParamName(0)); + + case STORE_BATCH_TAGS: + return MessageFormat.format( + "INSERT INTO BatchTagTable (Tags) VALUES (:{0})", + QueryType.STORE_BATCH_TAGS.getParamName(0)); + + case GET_BATCH_TAGS: + return MessageFormat.format( + "SELECT Tags FROM BatchTagTable WHERE BatchId = :{0}", + QueryType.GET_BATCH_TAGS.getParamName(0)); + + case ADD_ENTRY_NUM_TO_BATCH: + return MessageFormat.format( + "UPDATE BatchTable SET EntryNum = :{1} WHERE BatchId = :{0}", + QueryType.ADD_ENTRY_NUM_TO_BATCH.getParamName(0), + QueryType.ADD_ENTRY_NUM_TO_BATCH.getParamName(1)); + default: throw new IllegalArgumentException("Cannot serve a query of type " + queryType); } @@ -52,19 +125,34 @@ public class SQLiteQueryProvider implements BulletinBoardSQLServer.SQLQueryProvi switch(filterType) { case EXACT_ENTRY: return "MsgTable.EntryNum = :EntryNum" + serialString; + case MAX_ENTRY: return "MsgTable.EntryNum <= :EntryNum" + serialString; + + case MIN_ENTRY: + return "MsgTable.EntryNum <= :EntryNum" + serialString; + case MAX_MESSAGES: return "LIMIT = :Limit" + serialString; + case MSG_ID: return "MsgTable.MsgId = :MsgId" + serialString; + case SIGNER_ID: return "EXISTS (SELECT 1 FROM SignatureTable" + " WHERE SignatureTable.SignerId = :SignerId" + serialString + " AND SignatureTable.EntryNum = MsgTable.EntryNum)"; + case TAG: return "EXISTS (SELECT 1 FROM TagTable" + " INNER JOIN MsgTagTable ON TagTable.TagId = MsgTagTable.TagId" + " WHERE TagTable.Tag = :Tag" + serialString + " AND MsgTagTable.EntryNum = MsgTable.EntryNum)"; + + case BEFORE_TIME: + return "MsgTable.ExactTime <= :TimeStamp"; + + case AFTER_TIME: + return "MsgTable.ExactTime >= :TimeStamp"; + default: throw new IllegalArgumentException("Cannot serve a filter of type " + filterType); } @@ -73,15 +161,33 @@ public class SQLiteQueryProvider implements BulletinBoardSQLServer.SQLQueryProvi @Override public String getConditionParamTypeName(FilterType filterType) throws IllegalArgumentException { - return null; //TODO: write this. + + switch(filterType) { + case EXACT_ENTRY: // Go through + case MAX_ENTRY: // Go through + case MIN_ENTRY: // Go through + case MAX_MESSAGES: + return "INTEGER"; + + case MSG_ID: // Go through + case SIGNER_ID: + return "BLOB"; + + case TAG: + return "VARCHAR"; + + default: + throw new IllegalArgumentException("Cannot serve a filter of type " + filterType); + } + } @Override public DataSource getDataSource() { - // TODO: Fix this - SQLiteDataSource dataSource = new SQLiteDataSource(); + + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName("org.sqlite.JDBC"); dataSource.setUrl("jdbc:sqlite:" + dbName); - dataSource.setDatabaseName("meerkat"); //TODO: Make generic return dataSource; } @@ -94,14 +200,22 @@ public class SQLiteQueryProvider implements BulletinBoardSQLServer.SQLQueryProvi list.add("CREATE TABLE IF NOT EXISTS MsgTable (EntryNum INTEGER PRIMARY KEY, MsgId BLOB UNIQUE, Msg BLOB)"); list.add("CREATE TABLE IF NOT EXISTS TagTable (TagId INTEGER PRIMARY KEY, Tag varchar(50) UNIQUE)"); - list.add("CREATE TABLE IF NOT EXISTS MsgTagTable (EntryNum BLOB, TagId INTEGER, FOREIGN KEY (EntryNum)" - + " REFERENCES MsgTable(EntryNum), FOREIGN KEY (TagId) REFERENCES TagTable(TagId), UNIQUE (EntryNum, TagID))"); + list.add("CREATE TABLE IF NOT EXISTS MsgTagTable (EntryNum BLOB, TagId INTEGER," + + " FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum) ON DELETE CASCADE," + + " FOREIGN KEY (TagId) REFERENCES TagTable(TagId) ON DELETE CASCADE," + + " UNIQUE (EntryNum, TagID))"); list.add("CREATE TABLE IF NOT EXISTS SignatureTable (EntryNum INTEGER, SignerId BLOB, Signature BLOB," - + " FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum))"); + + " FOREIGN KEY (EntryNum) REFERENCES MsgTable(EntryNum) ON DELETE CASCADE," + + " UNIQUE(SignerId, EntryNum))"); list.add("CREATE INDEX IF NOT EXISTS SignerIndex ON SignatureTable(SignerId)"); - list.add("CREATE UNIQUE INDEX IF NOT EXISTS SignerIndex ON SignatureTable(SignerId, EntryNum)"); + + list.add("CREATE TABLE IF NOT EXISTS BatchTagTable (BatchId INTEGER PRIMARY KEY, Tags BLOB)"); + + list.add("CREATE TABLE IF NOT EXISTS BatchTable (BatchId INTEGER, EntryNum INTEGER, SerialNum INTEGER, Data BLOB," + + " UNIQUE(BatchId, SerialNum)," + + " FOREIGN KEY (BatchId) REFERENCES BatchTagTable(BatchId) ON DELETE CASCADE)"); return list; } diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/BatchDataCallbackHandler.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/BatchDataCallbackHandler.java new file mode 100644 index 0000000..69d7bae --- /dev/null +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/BatchDataCallbackHandler.java @@ -0,0 +1,30 @@ +package meerkat.bulletinboard.sqlserver.mappers; + +import meerkat.comm.MessageOutputStream; +import meerkat.protobuf.BulletinBoardAPI.BatchChunk; +import org.springframework.jdbc.core.RowCallbackHandler; + +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Created by Arbel Deutsch Peled on 19-Dec-15. + */ +public class BatchDataCallbackHandler implements RowCallbackHandler { + + private final MessageOutputStream out; + + public BatchDataCallbackHandler(MessageOutputStream out) { + this.out = out; + } + + @Override + public void processRow(ResultSet rs) throws SQLException { + try { + out.writeMessage(BatchChunk.parseFrom(rs.getBytes(1))); + } catch (IOException e) { + //TODO: Log + } + } +} diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/BatchDataDigestHandler.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/BatchDataDigestHandler.java new file mode 100644 index 0000000..ae55d5c --- /dev/null +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/BatchDataDigestHandler.java @@ -0,0 +1,31 @@ +package meerkat.bulletinboard.sqlserver.mappers; + +import meerkat.bulletinboard.BulletinBoardDigest; +import meerkat.protobuf.BulletinBoardAPI.BatchChunk; +import org.springframework.jdbc.core.RowCallbackHandler; + +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Created by Arbel Deutsch Peled on 19-Dec-15. + */ +public class BatchDataDigestHandler implements RowCallbackHandler { + + private final BulletinBoardDigest digest; + + public BatchDataDigestHandler(BulletinBoardDigest digest) { + this.digest = digest; + } + + @Override + public void processRow(ResultSet rs) throws SQLException { + try { + BatchChunk batchChunk = BatchChunk.parseFrom(rs.getBytes(1)); + digest.update(batchChunk.getData().toByteArray()); + } catch (IOException e) { + //TODO: Log + } + } +} diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/BeginBatchMessageMapper.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/BeginBatchMessageMapper.java new file mode 100644 index 0000000..0f72ec9 --- /dev/null +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/BeginBatchMessageMapper.java @@ -0,0 +1,24 @@ +package meerkat.bulletinboard.sqlserver.mappers; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.protobuf.BulletinBoardAPI.BeginBatchMessage; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Created by Arbel Deutsch Peled on 20-Dec-15. + */ +public class BeginBatchMessageMapper implements RowMapper { + + @Override + public BeginBatchMessage mapRow(ResultSet rs, int rowNum) throws SQLException { + try { + return BeginBatchMessage.newBuilder().mergeFrom(rs.getBytes(1)).build(); + } catch (InvalidProtocolBufferException e) { + return null; + } + } + +} diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/EntryNumMapper.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/LongMapper.java similarity index 87% rename from bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/EntryNumMapper.java rename to bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/LongMapper.java index 478c39e..1ec0d98 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/EntryNumMapper.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/LongMapper.java @@ -9,7 +9,7 @@ import java.sql.SQLException; /** * Created by Arbel Deutsch Peled on 11-Dec-15. */ -public class EntryNumMapper implements RowMapper { +public class LongMapper implements RowMapper { @Override public Long mapRow(ResultSet rs, int rowNum) throws SQLException { diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/MessageCallbackHandler.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/MessageCallbackHandler.java new file mode 100644 index 0000000..71ba742 --- /dev/null +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/MessageCallbackHandler.java @@ -0,0 +1,80 @@ +package meerkat.bulletinboard.sqlserver.mappers; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer.*; +import meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer.SQLQueryProvider.*; +import meerkat.comm.MessageOutputStream; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.protobuf.Crypto; +import org.springframework.jdbc.core.RowCallbackHandler; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 21-Feb-16. + */ +public class MessageCallbackHandler implements RowCallbackHandler { + + private final NamedParameterJdbcTemplate jdbcTemplate; + private final SQLQueryProvider sqlQueryProvider; + private final MessageOutputStream out; + + public MessageCallbackHandler(NamedParameterJdbcTemplate jdbcTemplate, SQLQueryProvider sqlQueryProvider, MessageOutputStream out) { + + this.jdbcTemplate = jdbcTemplate; + this.sqlQueryProvider = sqlQueryProvider; + this.out = out; + + } + + @Override + public void processRow(ResultSet rs) throws SQLException { + + BulletinBoardMessage.Builder result; + + try { + + result = BulletinBoardMessage.newBuilder() + .setEntryNum(rs.getLong(1)) + .setMsg(UnsignedBulletinBoardMessage.parseFrom(rs.getBytes(2))); + + + } catch (InvalidProtocolBufferException e) { + //TODO: log + return; + } + + // Retrieve signatures + + MapSqlParameterSource sqlParameterSource = new MapSqlParameterSource(); + sqlParameterSource.addValue(QueryType.GET_SIGNATURES.getParamName(0), result.getEntryNum()); + + List signatures = jdbcTemplate.query( + sqlQueryProvider.getSQLString(QueryType.GET_SIGNATURES), + sqlParameterSource, + new SignatureMapper()); + + // Append signatures + result.addAllSig(signatures); + + // Finalize message and add to message list. + + try { + + out.writeMessage(result.build()); + + } catch (IOException e) { + + //TODO: log + e.printStackTrace(); + + } + + } + +} diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/MessageStubCallbackHandler.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/MessageStubCallbackHandler.java new file mode 100644 index 0000000..f81cc76 --- /dev/null +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/MessageStubCallbackHandler.java @@ -0,0 +1,59 @@ +package meerkat.bulletinboard.sqlserver.mappers; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer.SQLQueryProvider.QueryType; +import meerkat.comm.MessageOutputStream; +import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage; +import meerkat.protobuf.BulletinBoardAPI.UnsignedBulletinBoardMessage; +import meerkat.protobuf.Crypto; +import meerkat.util.BulletinBoardUtils; +import org.springframework.jdbc.core.RowCallbackHandler; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 21-Feb-16. + */ +public class MessageStubCallbackHandler implements RowCallbackHandler { + + private final MessageOutputStream out; + + public MessageStubCallbackHandler(MessageOutputStream out) { + + this.out = out; + + } + + @Override + public void processRow(ResultSet rs) throws SQLException { + + BulletinBoardMessage result; + + result = BulletinBoardMessage.newBuilder() + .setEntryNum(rs.getLong(1)) + .setMsg(UnsignedBulletinBoardMessage.newBuilder() + .setData(ByteString.copyFrom(rs.getBytes(2))) + .setTimestamp(BulletinBoardUtils.toTimestampProto(rs.getTimestamp(3))) + .build()) + .build(); + + try { + + out.writeMessage(result); + + } catch (IOException e) { + + //TODO: log + e.printStackTrace(); + + } + + } + +} diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/MessageStubMapper.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/MessageStubMapper.java new file mode 100644 index 0000000..e9174e5 --- /dev/null +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/MessageStubMapper.java @@ -0,0 +1,31 @@ +package meerkat.bulletinboard.sqlserver.mappers; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage; +import meerkat.protobuf.BulletinBoardAPI.UnsignedBulletinBoardMessage; +import meerkat.util.BulletinBoardUtils; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Created by Arbel Deutsch Peled on 11-Dec-15. + */ +public class MessageStubMapper implements RowMapper { + + @Override + public BulletinBoardMessage mapRow(ResultSet rs, int rowNum) throws SQLException { + + return BulletinBoardMessage.newBuilder() + .setEntryNum(rs.getLong(1)) + .setMsg(UnsignedBulletinBoardMessage.newBuilder() + .setMsgId(ByteString.copyFrom(rs.getBytes(2))) + .setTimestamp(BulletinBoardUtils.toTimestampProto(rs.getTimestamp(3))) + .build()) + .build(); + + } + +} diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/StringMapper.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/StringMapper.java new file mode 100644 index 0000000..c5b1d85 --- /dev/null +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/sqlserver/mappers/StringMapper.java @@ -0,0 +1,18 @@ +package meerkat.bulletinboard.sqlserver.mappers; + +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Created by Arbel Deutsch Peled on 20-Dec-15. + */ +public class StringMapper implements RowMapper { + + @Override + public String mapRow(ResultSet rs, int rowNum) throws SQLException { + return rs.getString(1); + } + +} diff --git a/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java b/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java index b3fc03c..8e9d161 100644 --- a/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java +++ b/bulletin-board-server/src/main/java/meerkat/bulletinboard/webapp/BulletinBoardWebApp.java @@ -3,27 +3,32 @@ package meerkat.bulletinboard.webapp; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; +import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.StreamingOutput; +import com.google.protobuf.BoolValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; import meerkat.bulletinboard.BulletinBoardServer; import meerkat.bulletinboard.sqlserver.BulletinBoardSQLServer; import meerkat.bulletinboard.sqlserver.H2QueryProvider; import meerkat.bulletinboard.sqlserver.MySQLQueryProvider; import meerkat.bulletinboard.sqlserver.SQLiteQueryProvider; import meerkat.comm.CommunicationException; -import meerkat.protobuf.BulletinBoardAPI.BoolMsg; -import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage; -import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessageList; -import meerkat.protobuf.BulletinBoardAPI.MessageFilterList; -import meerkat.rest.Constants; +import meerkat.comm.MessageOutputStream; +import meerkat.protobuf.BulletinBoardAPI.*; +import static meerkat.bulletinboard.BulletinBoardConstants.*; +import static meerkat.rest.Constants.*; -@Path(Constants.BULLETIN_BOARD_SERVER_PATH) +import java.io.IOException; +import java.io.OutputStream; + +/** + * An implementation of the BulletinBoardServer which functions as a WebApp + */ +@Path(BULLETIN_BOARD_SERVER_PATH) public class BulletinBoardWebApp implements BulletinBoardServer, ServletContextListener{ private static final String BULLETIN_BOARD_ATTRIBUTE_NAME = "bulletinBoard"; @@ -39,14 +44,6 @@ public class BulletinBoardWebApp implements BulletinBoardServer, ServletContextL bulletinBoard = (BulletinBoardServer) servletContext.getAttribute(BULLETIN_BOARD_ATTRIBUTE_NAME); } - /** - * This is the BulletinBoard init method. - */ - @Override - public void init(String meerkatDB) throws CommunicationException { - bulletinBoard.init(meerkatDB); - } - @Override public void contextInitialized(ServletContextEvent servletContextEvent) { ServletContext servletContext = servletContextEvent.getServletContext(); @@ -72,31 +69,178 @@ public class BulletinBoardWebApp implements BulletinBoardServer, ServletContextL } try { - init(dbName); + bulletinBoard.init(); servletContext.setAttribute(BULLETIN_BOARD_ATTRIBUTE_NAME, bulletinBoard); } catch (CommunicationException e) { System.err.println(e.getMessage()); } } - @Path(Constants.POST_MESSAGE_PATH) + @Path(POST_MESSAGE_PATH) @POST - @Consumes(Constants.MEDIATYPE_PROTOBUF) - @Produces(Constants.MEDIATYPE_PROTOBUF) + @Consumes(MEDIATYPE_PROTOBUF) + @Produces(MEDIATYPE_PROTOBUF) @Override - public BoolMsg postMessage(BulletinBoardMessage msg) throws CommunicationException { + public BoolValue postMessage(BulletinBoardMessage msg) throws CommunicationException { init(); return bulletinBoard.postMessage(msg); } - - @Path(Constants.READ_MESSAGES_PATH) - @POST - @Consumes(Constants.MEDIATYPE_PROTOBUF) - @Produces(Constants.MEDIATYPE_PROTOBUF) + @Override - public BulletinBoardMessageList readMessages(MessageFilterList filterList) throws CommunicationException { + public void readMessages(MessageFilterList filterList, MessageOutputStream out) throws CommunicationException { init(); - return bulletinBoard.readMessages(filterList); + bulletinBoard.readMessages(filterList, out); + } + + @Path(COUNT_MESSAGES_PATH) + @POST + @Consumes(MEDIATYPE_PROTOBUF) + @Produces(MEDIATYPE_PROTOBUF) + @Override + public Int32Value getMessageCount(MessageFilterList filterList) throws CommunicationException { + init(); + return bulletinBoard.getMessageCount(filterList); + } + + + @Path(READ_MESSAGES_PATH) + @POST + @Consumes(MEDIATYPE_PROTOBUF) + /** + * Wrapper for the readMessages method which streams the output into the response + */ + public StreamingOutput readMessages(final MessageFilterList filterList) { + + return new StreamingOutput() { + + @Override + public void write(OutputStream output) throws IOException, WebApplicationException { + MessageOutputStream out = new MessageOutputStream<>(output); + + try { + init(); + bulletinBoard.readMessages(filterList, out); + } catch (CommunicationException e) { + //TODO: Log + out.writeMessage(null); + } + } + + }; + + } + + @Path(BEGIN_BATCH_PATH) + @POST + @Consumes(MEDIATYPE_PROTOBUF) + @Produces(MEDIATYPE_PROTOBUF) + @Override + public Int64Value beginBatch(BeginBatchMessage message) { + try { + init(); + return bulletinBoard.beginBatch(message); + } catch (CommunicationException e) { + System.err.println(e.getMessage()); + return null; + } + } + + @Path(POST_BATCH_PATH) + @POST + @Consumes(MEDIATYPE_PROTOBUF) + @Produces(MEDIATYPE_PROTOBUF) + @Override + public BoolValue postBatchMessage(BatchMessage batchMessage) { + try { + init(); + return bulletinBoard.postBatchMessage(batchMessage); + } catch (CommunicationException e) { + System.err.println(e.getMessage()); + return null; + } + } + + @Path(CLOSE_BATCH_PATH) + @POST + @Consumes(MEDIATYPE_PROTOBUF) + @Produces(MEDIATYPE_PROTOBUF) + @Override + public BoolValue closeBatch(CloseBatchMessage message) { + try { + init(); + return bulletinBoard.closeBatch(message); + } catch (CommunicationException e) { + System.err.println(e.getMessage()); + return null; + } + } + + + @Override + public void readBatch(BatchQuery batchQuery, MessageOutputStream out) throws CommunicationException, IllegalArgumentException { + try { + init(); + bulletinBoard.readBatch(batchQuery, out); + } catch (CommunicationException | IllegalArgumentException e) { + System.err.println(e.getMessage()); + } + } + + @Path(GENERATE_SYNC_QUERY_PATH) + @POST + @Consumes(MEDIATYPE_PROTOBUF) + @Produces(MEDIATYPE_PROTOBUF) + @Override + public SyncQuery generateSyncQuery(GenerateSyncQueryParams generateSyncQueryParams) throws CommunicationException { + try { + init(); + return bulletinBoard.generateSyncQuery(generateSyncQueryParams); + } catch (CommunicationException | IllegalArgumentException e) { + System.err.println(e.getMessage()); + return null; + } + } + + @Path(READ_BATCH_PATH) + @POST + @Consumes(MEDIATYPE_PROTOBUF) + /** + * Wrapper for the readBatch method which streams the output into the response + */ + public StreamingOutput readBatch(final BatchQuery batchQuery) { + + return new StreamingOutput() { + + @Override + public void write(OutputStream output) throws IOException, WebApplicationException { + MessageOutputStream out = new MessageOutputStream<>(output); + + try { + init(); + bulletinBoard.readBatch(batchQuery, out); + } catch (CommunicationException e) { + //TODO: Log + out.writeMessage(null); + } + } + + }; + + } + + @Path(SYNC_QUERY_PATH) + @POST + @Consumes(MEDIATYPE_PROTOBUF) + @Produces(MEDIATYPE_PROTOBUF) + @Override + public SyncQueryResponse querySync(SyncQuery syncQuery) throws CommunicationException { + try{ + init(); + return bulletinBoard.querySync(syncQuery); + } catch (CommunicationException | IllegalArgumentException e) { + System.err.println(e.getMessage()); + return null; + } } @Override @@ -121,4 +265,4 @@ public class BulletinBoardWebApp implements BulletinBoardServer, ServletContextL close(); } -} +} \ No newline at end of file diff --git a/bulletin-board-server/src/main/proto/meerkat/bulletin_board_server.proto b/bulletin-board-server/src/main/proto/meerkat/bulletin_board_server.proto deleted file mode 100644 index e31485b..0000000 --- a/bulletin-board-server/src/main/proto/meerkat/bulletin_board_server.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto3"; - -package meerkat; - -option java_package = "meerkat.protobuf"; - -message Boolean { - bool value = 1; -} \ No newline at end of file diff --git a/bulletin-board-server/src/main/webapp/WEB-INF/web.xml b/bulletin-board-server/src/main/webapp/WEB-INF/web.xml index 2198c07..226aa3b 100644 --- a/bulletin-board-server/src/main/webapp/WEB-INF/web.xml +++ b/bulletin-board-server/src/main/webapp/WEB-INF/web.xml @@ -31,7 +31,7 @@ mypass dbType - SQLite + H2 meerkat.bulletinboard.webapp.BulletinBoardWebApp diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/BulletinBoardSQLServerIntegrationTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/BulletinBoardSQLServerIntegrationTest.java index 838adcc..8d5e0c0 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/BulletinBoardSQLServerIntegrationTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/BulletinBoardSQLServerIntegrationTest.java @@ -1,11 +1,16 @@ package meerkat.bulletinboard; +import com.google.protobuf.BoolValue; import com.google.protobuf.ByteString; import com.google.protobuf.TextFormat; +import com.google.protobuf.Timestamp; +import meerkat.comm.MessageInputStream; import meerkat.protobuf.Crypto.*; import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.protobuf.Comm.*; +import static meerkat.bulletinboard.BulletinBoardConstants.*; import meerkat.rest.Constants; import meerkat.rest.ProtobufMessageBodyReader; import meerkat.rest.ProtobufMessageBodyWriter; @@ -18,6 +23,8 @@ import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Response; +import java.io.InputStream; +import java.util.List; public class BulletinBoardSQLServerIntegrationTest { @@ -43,19 +50,29 @@ public class BulletinBoardSQLServerIntegrationTest { byte[] b3 = {(byte) 21, (byte) 22, (byte) 23, (byte) 24}; byte[] b4 = {(byte) 4, (byte) 5, (byte) 100, (byte) -50, (byte) 0}; + Timestamp t1 = Timestamp.newBuilder() + .setSeconds(8276482) + .setNanos(4314) + .build(); + + Timestamp t2 = Timestamp.newBuilder() + .setSeconds(987591) + .setNanos(1513) + .build(); + WebTarget webTarget; Response response; - BoolMsg bool; + BoolValue bool; BulletinBoardMessage msg; MessageFilterList filterList; - BulletinBoardMessageList msgList; + List msgList; // Test writing mechanism - System.err.println("******** Testing: " + Constants.POST_MESSAGE_PATH); - webTarget = client.target(BASE_URL).path(Constants.BULLETIN_BOARD_SERVER_PATH).path(Constants.POST_MESSAGE_PATH); + System.err.println("******** Testing: " + POST_MESSAGE_PATH); + webTarget = client.target(BASE_URL).path(BULLETIN_BOARD_SERVER_PATH).path(POST_MESSAGE_PATH); System.err.println(webTarget.getUri()); msg = BulletinBoardMessage.newBuilder() @@ -63,6 +80,7 @@ public class BulletinBoardSQLServerIntegrationTest { .addTag("Signature") .addTag("Trustee") .setData(ByteString.copyFrom(b1)) + .setTimestamp(t1) .build()) .addSig(Signature.newBuilder() .setType(SignatureType.DSA) @@ -78,7 +96,7 @@ public class BulletinBoardSQLServerIntegrationTest { response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(msg, Constants.MEDIATYPE_PROTOBUF)); System.err.println(response); - bool = response.readEntity(BoolMsg.class); + bool = response.readEntity(BoolValue.class); assert bool.getValue(); msg = BulletinBoardMessage.newBuilder() @@ -86,6 +104,7 @@ public class BulletinBoardSQLServerIntegrationTest { .addTag("Vote") .addTag("Trustee") .setData(ByteString.copyFrom(b4)) + .setTimestamp(t2) .build()) .addSig(Signature.newBuilder() .setType(SignatureType.ECDSA) @@ -96,13 +115,13 @@ public class BulletinBoardSQLServerIntegrationTest { response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(msg, Constants.MEDIATYPE_PROTOBUF)); System.err.println(response); - bool = response.readEntity(BoolMsg.class); + bool = response.readEntity(BoolValue.class); assert bool.getValue(); // Test reading mechanism - System.err.println("******** Testing: " + Constants.READ_MESSAGES_PATH); - webTarget = client.target(BASE_URL).path(Constants.BULLETIN_BOARD_SERVER_PATH).path(Constants.READ_MESSAGES_PATH); + System.err.println("******** Testing: " + READ_MESSAGES_PATH); + webTarget = client.target(BASE_URL).path(BULLETIN_BOARD_SERVER_PATH).path(READ_MESSAGES_PATH); filterList = MessageFilterList.newBuilder() .addFilter( MessageFilter.newBuilder() @@ -112,13 +131,20 @@ public class BulletinBoardSQLServerIntegrationTest { ) .build(); - response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(filterList, Constants.MEDIATYPE_PROTOBUF)); - System.err.println(response); - msgList = response.readEntity(BulletinBoardMessageList.class); - System.err.println("List size: " + msgList.getMessageCount()); + InputStream in = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(filterList, Constants.MEDIATYPE_PROTOBUF), InputStream.class); + + MessageInputStream inputStream = + MessageInputStream.MessageInputStreamFactory.createMessageInputStream(in, BulletinBoardMessage.class); + + msgList = inputStream.asList(); + System.err.println("List size: " + msgList.size()); System.err.println("This is the list:"); - System.err.println(TextFormat.printToString(msgList)); - assert msgList.getMessageCount() == 1; + + for (BulletinBoardMessage message : msgList) { + System.err.println(TextFormat.printToString(message)); + } + + assert msgList.size() == 1; } } diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java index 4799e0d..eafd80c 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/GenericBulletinBoardServerTest.java @@ -1,9 +1,12 @@ package meerkat.bulletinboard; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.management.ManagementFactory; import java.lang.management.ThreadMXBean; +import java.lang.reflect.InvocationTargetException; import java.math.BigInteger; import java.security.InvalidKeyException; import java.security.KeyStore; @@ -12,26 +15,32 @@ import java.security.NoSuchAlgorithmException; import java.security.SignatureException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; -import java.util.List; -import java.util.Random; +import java.util.*; +import com.google.protobuf.BoolValue; import com.google.protobuf.ByteString; +import com.google.protobuf.Int64Value; +import com.google.protobuf.Timestamp; import meerkat.comm.CommunicationException; +import meerkat.comm.MessageInputStream; +import meerkat.comm.MessageOutputStream; +import meerkat.comm.MessageInputStream.MessageInputStreamFactory; import meerkat.crypto.concrete.ECDSASignature; -import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage; -import meerkat.protobuf.BulletinBoardAPI.FilterType; -import meerkat.protobuf.BulletinBoardAPI.MessageFilter; -import meerkat.protobuf.BulletinBoardAPI.MessageFilterList; -import meerkat.protobuf.BulletinBoardAPI.UnsignedBulletinBoardMessage; +import meerkat.crypto.concrete.SHA256Digest; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.util.BulletinBoardMessageComparator; +import meerkat.util.BulletinBoardMessageGenerator; +import meerkat.util.BulletinBoardUtils; import static org.junit.Assert.*; import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; public class GenericBulletinBoardServerTest { protected BulletinBoardServer bulletinBoardServer; - private ECDSASignature signers[]; + private GenericBulletinBoardSignature[] signers; private ByteString[] signerIDs; private Random random; @@ -51,18 +60,18 @@ public class GenericBulletinBoardServerTest { private String[] tags; private byte[][] data; + private List batches; + private final ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); // Used to time the tests + private BulletinBoardMessageGenerator bulletinBoardMessageGenerator; + + private BulletinBoardDigest digest; + + private BulletinBoardMessageComparator comparator; + /** * @param bulletinBoardServer is an initialized server. - * @throws InstantiationException - * @throws IllegalAccessException - * @throws CertificateException - * @throws KeyStoreException - * @throws NoSuchAlgorithmException - * @throws IOException - * @throws UnrecoverableKeyException - * @throws CommunicationException */ public void init(BulletinBoardServer bulletinBoardServer) { @@ -71,10 +80,10 @@ public class GenericBulletinBoardServerTest { this.bulletinBoardServer = bulletinBoardServer; - signers = new ECDSASignature[2]; + signers = new GenericBulletinBoardSignature[2]; signerIDs = new ByteString[signers.length]; - signers[0] = new ECDSASignature(); - signers[1] = new ECDSASignature(); + signers[0] = new GenericBulletinBoardSignature(new ECDSASignature()); + signers[1] = new GenericBulletinBoardSignature(new ECDSASignature()); InputStream keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE); char[] password = KEYFILE_PASSWORD1.toCharArray(); @@ -115,12 +124,23 @@ public class GenericBulletinBoardServerTest { System.err.println("Couldn't find signing key " + e.getMessage()); fail("Couldn't find signing key " + e.getMessage()); } - - random = new Random(0); // We use insecure randomness in tests for repeatability + + // We use insecure randomness in tests for repeatability + random = new Random(0); + bulletinBoardMessageGenerator = new BulletinBoardMessageGenerator(random); + + digest = new GenericBulletinBoardDigest(new SHA256Digest()); + + comparator = new BulletinBoardMessageComparator(); long end = threadBean.getCurrentThreadCpuTime(); System.err.println("Finished initializing GenericBulletinBoardServerTest"); System.err.println("Time of operation: " + (end - start)); + + // Initialize Batch variables + + batches = new ArrayList<>(10); + } private byte randomByte(){ @@ -170,7 +190,11 @@ public class GenericBulletinBoardServerTest { for (i = 1; i <= MESSAGE_NUM; i++) { unsignedMsgBuilder = UnsignedBulletinBoardMessage.newBuilder() - .setData(ByteString.copyFrom(data[i - 1])); + .setData(ByteString.copyFrom(data[i - 1])) + .setTimestamp(Timestamp.newBuilder() + .setSeconds(i) + .setNanos(i) + .build()); // Add tags based on bit-representation of message number. @@ -230,28 +254,39 @@ public class GenericBulletinBoardServerTest { System.err.println("Starting to test tag and signature mechanism"); long start = threadBean.getCurrentThreadCpuTime(); - List messages; + List messages = new LinkedList<>(); // Check tag mechanism - + for (int i = 0 ; i < TAG_NUM ; i++){ // Retrieve messages having tag i try { - messages = bulletinBoardServer.readMessages( - MessageFilterList.newBuilder() - .addFilter(MessageFilter.newBuilder() - .setType(FilterType.TAG) - .setTag(tags[i]) - .build() - ) - .build() - ) - .getMessageList(); + MessageFilterList filterList = MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.TAG) + .setTag(tags[i]) + .build() + ) + .build(); - } catch (CommunicationException e) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + bulletinBoardServer.readMessages(filterList, new MessageOutputStream(outputStream)); + + MessageInputStream inputStream = + MessageInputStreamFactory.createMessageInputStream(new ByteArrayInputStream( + outputStream.toByteArray()), + BulletinBoardMessage.class); + + messages = inputStream.asList(); + + } catch (CommunicationException | IOException e) { + fail(e.getMessage()); + return; + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { fail(e.getMessage()); return; } @@ -328,11 +363,26 @@ public class GenericBulletinBoardServerTest { ); try { - messages = bulletinBoardServer.readMessages(filterListBuilder.build()).getMessageList(); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + bulletinBoardServer.readMessages(filterListBuilder.build(), new MessageOutputStream(outputStream)); + + MessageInputStream inputStream = + MessageInputStreamFactory.createMessageInputStream(new ByteArrayInputStream( + outputStream.toByteArray()), + BulletinBoardMessage.class); + + messages = inputStream.asList(); + } catch (CommunicationException e) { System.err.println("Failed retrieving multi-tag messages from DB: " + e.getMessage()); fail("Failed retrieving multi-tag messages from DB: " + e.getMessage()); return; + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | IOException e) { + System.err.println("Falied to read from stream while retrieving multi-tag messages: " + e.getMessage()); + fail("Falied to read from stream while retrieving multi-tag messages: " + e.getMessage()); + return; } expectedMsgCount /= 2; @@ -359,11 +409,26 @@ public class GenericBulletinBoardServerTest { .build()); try { - messages = bulletinBoardServer.readMessages(filterListBuilder.build()).getMessageList(); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + bulletinBoardServer.readMessages(filterListBuilder.build(), new MessageOutputStream(outputStream)); + + MessageInputStream inputStream = + MessageInputStreamFactory.createMessageInputStream(new ByteArrayInputStream( + outputStream.toByteArray()), + BulletinBoardMessage.class); + + messages = inputStream.asList(); + } catch (CommunicationException e) { System.err.println("Failed retrieving multi-signature message from DB: " + e.getMessage()); fail("Failed retrieving multi-signature message from DB: " + e.getMessage()); return; + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | IOException e) { + System.err.println("Falied to read from stream while retrieving multi-signature message: " + e.getMessage()); + fail("Falied to read from stream while retrieving multi-signature message: " + e.getMessage()); + return; } assertThat(messages.size(), is(MESSAGE_NUM / 4)); @@ -377,6 +442,207 @@ public class GenericBulletinBoardServerTest { System.err.println("Time of operation: " + (end - start)); } + + + private void postAsBatch(BulletinBoardMessage message, int chunkSize, boolean close) throws CommunicationException { + + List batchChunks = BulletinBoardUtils.breakToBatch(message, chunkSize); + BeginBatchMessage beginBatchMessage = BulletinBoardUtils.generateBeginBatchMessage(message); + + BoolValue result; + + // Begin batch + + Int64Value batchId = bulletinBoardServer.beginBatch(beginBatchMessage); + + assertThat("Was not able to open batch", batchId.getValue() != -1); + + // Post data + + BatchMessage batchMessage = BatchMessage.getDefaultInstance(); + + for (int i = 0 ; i < batchChunks.size() ; i++){ + + batchMessage = BatchMessage.newBuilder() + .setBatchId(batchId.getValue()) + .setSerialNum(i) + .setData(batchChunks.get(i)) + .build(); + + result = bulletinBoardServer.postBatchMessage(batchMessage); + + assertThat("Was not able to post batch message", result.getValue(), is(true)); + + } + + // Close batch + if (close) { + + CloseBatchMessage closeBatchMessage = BulletinBoardUtils.generateCloseBatchMessage(batchId, batchChunks.size(), message); + + result = bulletinBoardServer.closeBatch(closeBatchMessage); + + assertThat("Was not able to close batch", result.getValue(), is(true)); + + } + + } + + /** + * Posts a complete batch message + * @throws CommunicationException + */ + public void testPostBatch() throws CommunicationException, SignatureException { + + // Create data + final int BATCH_ID = 200; + final int DATA_SIZE = 10000; + final int CHUNK_SIZE = 100; + final int TAG_NUMBER = 10; + + Timestamp timestamp = Timestamp.newBuilder() + .setSeconds(5235000) + .setNanos(32541) + .build(); + + BulletinBoardMessage batch = bulletinBoardMessageGenerator.generateRandomMessage(signers, timestamp, DATA_SIZE, TAG_NUMBER); + + // Post batch + + postAsBatch(batch, CHUNK_SIZE, true); + + // Update locally stored batches + batches.add(batch); + + } + + public void testReadBatch() throws CommunicationException { + + for (BulletinBoardMessage message : batches) { + + try { + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + digest.update(message); + + MessageID msgId = digest.digestAsMessageID(); + + MessageFilterList messageFilterList = MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.MSG_ID) + .setId(msgId.getID()) + .build()) + .build(); + + bulletinBoardServer.readMessages(messageFilterList, new MessageOutputStream(outputStream)); + + MessageInputStream messageInputStream = + MessageInputStreamFactory.createMessageInputStream(new ByteArrayInputStream( + outputStream.toByteArray()), + BulletinBoardMessage.class); + + List messageList = messageInputStream.asList(); + + assertThat("No stub found for message ID " + msgId.getID().toStringUtf8(), messageList.size() == 1); + + BulletinBoardMessage stub = messageList.get(0); + + BatchQuery batchQuery = + BatchQuery.newBuilder() + .setMsgID(msgId) + .setStartPosition(0) + .build(); + + bulletinBoardServer.readBatch(batchQuery, new MessageOutputStream(outputStream)); + + MessageInputStream batchInputStream = + MessageInputStreamFactory.createMessageInputStream(new ByteArrayInputStream( + outputStream.toByteArray()), + BatchChunk.class); + + List batchChunkList = batchInputStream.asList(); + + BulletinBoardMessage retrievedMessage = BulletinBoardUtils.gatherBatch(stub, batchChunkList); + + assertThat("Non-matching batch data for batch " + msgId.getID().toStringUtf8(), + comparator.compare(message, retrievedMessage) == 0); + + } catch (IOException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + assertThat("Error reading batch data list from input stream", false); + } + + } + + } + + public void testSyncQuery() + throws SignatureException, CommunicationException, IOException,NoSuchMethodException, IllegalAccessException, InvocationTargetException { + + Checksum checksum = new SimpleChecksum(); + + Timestamp timestamp = Timestamp.newBuilder() + .setSeconds(1) + .setNanos(0) + .build(); + + BulletinBoardMessage newMessage = bulletinBoardMessageGenerator.generateRandomMessage(signers, timestamp, 10, 10); + + BoolValue result = bulletinBoardServer.postMessage(newMessage); + assertThat("Failed to post message to BB Server", result.getValue(), is(true)); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + digest.update(newMessage.getMsg()); + + ByteString messageID = ByteString.copyFrom(digest.digest()); + + MessageFilterList filterList = MessageFilterList.newBuilder() + .addFilter(MessageFilter.newBuilder() + .setType(FilterType.MSG_ID) + .setId(messageID) + .build()) + .build(); + + bulletinBoardServer.readMessages(filterList, new MessageOutputStream(outputStream)); + + MessageInputStream inputStream = + MessageInputStreamFactory.createMessageInputStream(new ByteArrayInputStream( + outputStream.toByteArray()), + BulletinBoardMessage.class); + + long lastEntry = inputStream.asList().get(0).getEntryNum(); + + SyncQuery syncQuery = SyncQuery.newBuilder() + .setFilterList(MessageFilterList.getDefaultInstance()) + .addQuery(SingleSyncQuery.newBuilder() + .setChecksum(2) + .setTimeOfSync(Timestamp.newBuilder() + .setSeconds(2) + .setNanos(0) + .build()) + .build()) + .build(); + + SyncQueryResponse queryResponse = bulletinBoardServer.querySync(syncQuery); + + assertThat("Sync query replies with positive sync when no sync was expected", queryResponse.getLastEntryNum(), is(equalTo(-1l))); + + syncQuery = SyncQuery.newBuilder() + .setFilterList(MessageFilterList.getDefaultInstance()) + .addQuery(SingleSyncQuery.newBuilder() + .setChecksum(checksum.getChecksum(messageID)) + .setTimeOfSync(timestamp) + .build()) + .build(); + + queryResponse = bulletinBoardServer.querySync(syncQuery); + + assertThat("Sync query reply contained wrong last entry number", lastEntry, is(equalTo(queryResponse.getLastEntryNum()))); + + assertThat("Sync query reply contained wrong timestamp", timestamp, is(equalTo(queryResponse.getLastTimeOfSync()))); + + } public void close(){ signers[0].clearSigningKey(); diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/H2BulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/H2BulletinBoardServerTest.java index ef19310..def0b41 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/H2BulletinBoardServerTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/H2BulletinBoardServerTest.java @@ -7,7 +7,6 @@ import meerkat.comm.CommunicationException; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.junit.runner.Result; import java.lang.management.ManagementFactory; import java.lang.management.ThreadMXBean; @@ -55,7 +54,7 @@ public class H2BulletinBoardServerTest { BulletinBoardServer bulletinBoardServer = new BulletinBoardSQLServer(queryProvider); try { - bulletinBoardServer.init(""); + bulletinBoardServer.init(); } catch (CommunicationException e) { System.err.println(e.getMessage()); @@ -107,6 +106,39 @@ public class H2BulletinBoardServerTest { System.err.println("Time of operation: " + (end - start)); } + @Test + public void testBatch() { + + final int BATCH_NUM = 20; + + try{ + for (int i = 0 ; i < BATCH_NUM ; i++) { + serverTest.testPostBatch(); + } + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + + try{ + serverTest.testReadBatch(); + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + + } + + @Test + public void testSyncQuery() { + try { + serverTest.testSyncQuery(); + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + } + @After public void close() { System.err.println("Starting to close H2BulletinBoardServerTest"); diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/MySQLBulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/MySQLBulletinBoardServerTest.java index e473931..abc0fc6 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/MySQLBulletinBoardServerTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/MySQLBulletinBoardServerTest.java @@ -11,7 +11,6 @@ import org.junit.Test; import java.lang.management.ManagementFactory; import java.lang.management.ThreadMXBean; import java.sql.Connection; -import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; import java.util.List; @@ -59,7 +58,7 @@ public class MySQLBulletinBoardServerTest { BulletinBoardServer bulletinBoardServer = new BulletinBoardSQLServer(queryProvider); try { - bulletinBoardServer.init(""); + bulletinBoardServer.init(); } catch (CommunicationException e) { System.err.println(e.getMessage()); @@ -111,6 +110,39 @@ public class MySQLBulletinBoardServerTest { System.err.println("Time of operation: " + (end - start)); } + @Test + public void testBatch() { + + final int BATCH_NUM = 20; + + try{ + for (int i = 0 ; i < BATCH_NUM ; i++) { + serverTest.testPostBatch(); + } + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + + try{ + serverTest.testReadBatch(); + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + + } + + @Test + public void testSyncQuery() { + try { + serverTest.testSyncQuery(); + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + } + @After public void close() { System.err.println("Starting to close MySQLBulletinBoardServerTest"); diff --git a/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteBulletinBoardServerTest.java b/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteBulletinBoardServerTest.java index 1d7aae0..18278b3 100644 --- a/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteBulletinBoardServerTest.java +++ b/bulletin-board-server/src/test/java/meerkat/bulletinboard/SQLiteBulletinBoardServerTest.java @@ -39,7 +39,7 @@ public class SQLiteBulletinBoardServerTest{ BulletinBoardServer bulletinBoardServer = new BulletinBoardSQLServer(new SQLiteQueryProvider(testFilename)); try { - bulletinBoardServer.init(""); + bulletinBoardServer.init(); } catch (CommunicationException e) { System.err.println(e.getMessage()); @@ -60,7 +60,7 @@ public class SQLiteBulletinBoardServerTest{ System.err.println("Time of operation: " + (end - start)); } - @Test +// @Test public void bulkTest() { System.err.println("Starting bulkTest of SQLiteBulletinBoardServerTest"); long start = threadBean.getCurrentThreadCpuTime(); @@ -91,6 +91,29 @@ public class SQLiteBulletinBoardServerTest{ System.err.println("Time of operation: " + (end - start)); } +// @Test + public void testBatch() { + + final int BATCH_NUM = 20; + + try{ + for (int i = 0 ; i < BATCH_NUM ; i++) { + serverTest.testPostBatch(); + } + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + + try{ + serverTest.testReadBatch(); + } catch (Exception e) { + System.err.println(e.getMessage()); + fail(e.getMessage()); + } + + } + @After public void close() { System.err.println("Starting to close SQLiteBulletinBoardServerTest"); diff --git a/distributed-key-generation/build.gradle b/distributed-key-generation/build.gradle new file mode 100644 index 0000000..c90a15a --- /dev/null +++ b/distributed-key-generation/build.gradle @@ -0,0 +1,223 @@ + +plugins { + id "us.kirchmeier.capsule" version "1.0.1" + id 'com.google.protobuf' version '0.7.0' +} + +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'idea' + +apply plugin: 'maven-publish' + +// Uncomment the lines below to define an application +// (this will also allow you to build a "fatCapsule" which includes +// the entire application, including all dependencies in a single jar) +//apply plugin: 'application' +//mainClassName='your.main.ApplicationClass' + +// Is this a snapshot version? +ext { isSnapshot = false } + +ext { + groupId = 'org.factcenter.meerkat' + nexusRepository = "https://cs.idc.ac.il/nexus/content/groups/${isSnapshot ? 'unstable' : 'public'}/" + + // Credentials for IDC nexus repositories (needed only for using unstable repositories and publishing) + // Should be set in ${HOME}/.gradle/gradle.properties + nexusUser = project.hasProperty('nexusUser') ? project.property('nexusUser') : "" + nexusPassword = project.hasProperty('nexusPassword') ? project.property('nexusPassword') : "" +} + +description = "TODO: Add a description" + +// Your project version +version = "0.0" + +version += "${isSnapshot ? '-SNAPSHOT' : ''}" + + +dependencies { + // Meerkat common + compile project(':meerkat-common') + + // Logging + compile 'org.slf4j:slf4j-api:1.7.7' + runtime 'ch.qos.logback:logback-classic:1.1.2' + runtime 'ch.qos.logback:logback-core:1.1.2' + + // Google protobufs + compile 'com.google.protobuf:protobuf-java:3.+' + + // Depend on test resources from meerkat-common + testCompile project(path: ':meerkat-common', configuration: 'testOutput') + + testCompile 'junit:junit:4.+' + + runtime 'org.codehaus.groovy:groovy:2.4.+' +} + + +/*==== You probably don't have to edit below this line =======*/ + +// Setup test configuration that can appear as a dependency in +// other subprojects +configurations { + testOutput.extendsFrom (testCompile) +} + +task testJar(type: Jar, dependsOn: testClasses) { + classifier = 'tests' + from sourceSets.test.output +} + +artifacts { + testOutput testJar +} + +// The run task added by the application plugin +// is also of type JavaExec. +tasks.withType(JavaExec) { + // Assign all Java system properties from + // the command line to the JavaExec task. + systemProperties System.properties +} + + +protobuf { + // Configure the protoc executable + protoc { + // Download from repositories + artifact = 'com.google.protobuf:protoc:3.+' + } +} + + +idea { + module { + project.sourceSets.each { sourceSet -> + + def srcDir = "${protobuf.generatedFilesBaseDir}/$sourceSet.name/java" + + println "Adding $srcDir" + // add protobuf generated sources to generated source dir. + if ("test".equals(sourceSet.name)) { + testSourceDirs += file(srcDir) + } else { + sourceDirs += file(srcDir) + } + generatedSourceDirs += file(srcDir) + + } + + // Don't exclude build directory + excludeDirs -= file(buildDir) + } +} + + +/*=================================== + * "Fat" Build targets + *===================================*/ + + +if (project.hasProperty('mainClassName') && (mainClassName != null)) { + + task mavenCapsule(type: MavenCapsule) { + description = "Generate a capsule jar that automatically downloads and caches dependencies when run." + applicationClass mainClassName + destinationDir = buildDir + } + + task fatCapsule(type: FatCapsule) { + description = "Generate a single capsule jar containing everything. Use -Pfatmain=... to override main class" + + destinationDir = buildDir + + def fatMain = hasProperty('fatmain') ? fatmain : mainClassName + + applicationClass fatMain + + def testJar = hasProperty('test') + + if (hasProperty('fatmain')) { + appendix = "fat-${fatMain}" + } else { + appendix = "fat" + } + + if (testJar) { + from sourceSets.test.output + } + } +} + + +/*=================================== + * Repositories + *===================================*/ + +repositories { + + // Prefer the local nexus repository (it may have 3rd party artifacts not found in mavenCentral) + maven { + url nexusRepository + + if (isSnapshot) { + credentials { username + password + + username nexusUser + password nexusPassword + } + } + } + + // Use local maven repository + mavenLocal() + + // Use 'maven central' for other dependencies. + mavenCentral() +} + +task "info" << { + println "Project: ${project.name}" +println "Description: ${project.description}" + println "--------------------------" + println "GroupId: $groupId" + println "Version: $version (${isSnapshot ? 'snapshot' : 'release'})" + println "" +} +info.description 'Print some information about project parameters' + + +/*=================================== + * Publishing + *===================================*/ + +publishing { + publications { + mavenJava(MavenPublication) { + groupId project.groupId + pom.withXml { + asNode().appendNode('description', project.description) + } + from project.components.java + + } + } + repositories { + maven { + url "https://cs.idc.ac.il/nexus/content/repositories/${project.isSnapshot ? 'snapshots' : 'releases'}" + credentials { username + password + + username nexusUser + password nexusPassword + } + } + } +} + + + diff --git a/distributed-key-generation/src/main/java/meerkat/crypto/dkg/comm/MessageHandler.java b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/comm/MessageHandler.java new file mode 100644 index 0000000..02c751b --- /dev/null +++ b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/comm/MessageHandler.java @@ -0,0 +1,47 @@ +package meerkat.crypto.dkg.comm; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.comm.Channel; +import meerkat.protobuf.Comm; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Created by Tzlil on 2/14/2016. + * + * an implementation of ReceiverCallback + */ +public abstract class MessageHandler implements Channel.ReceiverCallback { + final Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * fixed value for broadcasting + */ + public static final int BROADCAST = 0; + + /** + * Handle a broadcast (or unicast) message. + * If the message is invalid, the handler can throw an {@link InvalidProtocolBufferException}, in which + * case the message will simply be ignored. + * @param envelope + */ + public abstract void handleMessage(Comm.BroadcastMessage envelope) throws InvalidProtocolBufferException; + + /** + * Was this broadcastMessage was received by broadcast channel + * @param broadcastMessage + * @return broadcastMessage user destination == BROADCAST + */ + public boolean isBroadcast(Comm.BroadcastMessage broadcastMessage){ + return broadcastMessage.getDestination() == BROADCAST; + } + + @Override + public void receiveMessage(Comm.BroadcastMessage envelope) { + try { + handleMessage(envelope); + } catch (InvalidProtocolBufferException e) { + logger.warn("Received invalid protocol buffer from channel", e); + } + } +} diff --git a/distributed-key-generation/src/main/java/meerkat/crypto/dkg/comm/MessageUtils.java b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/comm/MessageUtils.java new file mode 100644 index 0000000..a0cb79a --- /dev/null +++ b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/comm/MessageUtils.java @@ -0,0 +1,36 @@ +package meerkat.crypto.dkg.comm; + +import meerkat.protobuf.DKG; + +/** + * Created by talm on 12/04/16. + */ +public class MessageUtils { + public static DKG.Payload createMessage(DKG.Payload.Type type) { + return DKG.Payload.newBuilder().setType(type).build(); + } + + public static DKG.Payload createMessage(DKG.Payload.Type type, DKG.ShareMessage share) { + return DKG.Payload.newBuilder().setType(type).setShare(share).build(); + } + + public static DKG.Payload createMessage(DKG.Payload.Type type, DKG.ShareMessage.Builder share) { + return DKG.Payload.newBuilder().setType(type).setShare(share).build(); + } + + public static DKG.Payload createMessage(DKG.Payload.Type type, DKG.IDMessage id) { + return DKG.Payload.newBuilder().setType(type).setId(id).build(); + } + + public static DKG.Payload createMessage(DKG.Payload.Type type, DKG.IDMessage.Builder id) { + return DKG.Payload.newBuilder().setType(type).setId(id).build(); + } + + public static DKG.Payload createMessage(DKG.Payload.Type type, DKG.CommitmentMessage commitment) { + return DKG.Payload.newBuilder().setType(type).setCommitment(commitment).build(); + } + + public static DKG.Payload createMessage(DKG.Payload.Type type, DKG.CommitmentMessage.Builder commitment) { + return DKG.Payload.newBuilder().setType(type).setCommitment(commitment).build(); + } +} diff --git a/distributed-key-generation/src/main/java/meerkat/crypto/dkg/feldman/Party.java b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/feldman/Party.java new file mode 100644 index 0000000..1b2cbe8 --- /dev/null +++ b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/feldman/Party.java @@ -0,0 +1,40 @@ +package meerkat.crypto.dkg.feldman; + +import meerkat.crypto.secretsharing.shamir.Polynomial; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Created by Tzlil on 3/14/2016. + * + * contains all relevant information on specific party during + * the run of Joint Feldamn protocol + */ +public class Party { + public final int id; + public Polynomial.Point share; + public ArrayList commitments; + public boolean doneFlag; + public Protocol.ComplaintState[] complaints; + public boolean aborted; + + /** + * + * @param id party identifier - 1 <= id <= n + * @param n number of parties in current run protocol + * @param t protocol's threshold + */ + public Party(int id, int n, int t) { + this.id = id; + this.share = null; + this.doneFlag = false; + this.complaints = new Protocol.ComplaintState[n]; + Arrays.fill(this.complaints, Protocol.ComplaintState.OK); + this.commitments = new ArrayList(t + 1); + for (int i = 0; i <= t ; i++){ + commitments.add(null); + } + this.aborted = false; + } +} diff --git a/distributed-key-generation/src/main/java/meerkat/crypto/dkg/feldman/Protocol.java b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/feldman/Protocol.java new file mode 100644 index 0000000..f612b07 --- /dev/null +++ b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/feldman/Protocol.java @@ -0,0 +1,369 @@ +package meerkat.crypto.dkg.feldman; + +import meerkat.comm.Channel; +import meerkat.crypto.secretsharing.feldman.VerifiableSecretSharing; +import meerkat.crypto.secretsharing.shamir.Polynomial; +import com.google.protobuf.ByteString; +import meerkat.protobuf.DKG; +import org.factcenter.qilin.primitives.Group; +import org.factcenter.qilin.util.ByteEncoder; + +import java.math.BigInteger; +import java.util.*; + +import static meerkat.crypto.dkg.comm.MessageUtils.*; + +/** + * Created by Tzlil on 3/14/2016. + * + * an implementation of JointFeldman distributed key generation protocol. + * + * allows set of n parties to generate random key with threshold t. + */ +public class Protocol extends VerifiableSecretSharing { + public enum ComplaintState { + /** + * No complaints, no response required at this point. + */ + OK, + + /** + * Party received complaint, waiting for response from party + */ + Waiting, + + /** + * Party gave invalid answer to complaint. + */ + Disqualified, + + /** + * Party received complaint, gave valid answer. + */ + NonDisqualified + } + + /** + * My share id. + */ + protected final int id; + + /** + * All parties participating in key generation. + * parties[id-1] has my info. + */ + private Party[] parties; + + /** + * communication object + */ + protected Channel channel; + + /** + * Encode/Decode group elements + */ + protected final ByteEncoder encoder; + + /** + * constructor + * @param q a large prime. + * @param t threshold. Any t+1 share holders can recover the secret, + * but any set of at most t share holders cannot + * @param n number of share holders + * @param zi secret, chosen from Zq + * @param random use for generate random polynomial + * @param group + * @param q a large prime dividing group order. + * @param g a generator of cyclic group of order q. + * the generated group is a subgroup of the given group. + * it must be chosen such that computing discrete logarithms is hard in this group. + * @param encoder Encode/Decode group elements (of type T) to/from byte array + */ + //TODO: why the use of regular Random? Should it be changed? + public Protocol(int t, int n, BigInteger zi, Random random, BigInteger q, T g + , Group group, int id, ByteEncoder encoder) { + super(t, n, zi, random, q, g,group); + this.id = id; + this.parties = new Party[n]; + for (int i = 1; i <= n ; i++){ + this.parties[i - 1] = new Party(i,n,t); + } + this.parties[id - 1].share = getShare(id); + this.encoder = encoder; + } + + /** + * setter + * @param channel + */ + public void setChannel(Channel channel){ + this.channel = channel; + } + + /** + * setter + * @param parties + */ + protected void setParties(Party[] parties){ + this.parties = parties; + } + + /** + * getter + * @return + */ + protected Party[] getParties(){ + return parties; + } + + /** + * stage1.1 according to the protocol + * Pi broadcasts Aik for k = 0,...,t. + */ + public void broadcastCommitments(){ + broadcastCommitments(commitmentsArrayList); + } + + /** + * pack commitments as messages and broadcast them + * @param commitments + */ + public void broadcastCommitments(ArrayList commitments){ + DKG.CommitmentMessage commitmentMessage; + for (int k = 0; k <= t ; k++){ + commitmentMessage = DKG.CommitmentMessage.newBuilder() + .setCommitment(ByteString.copyFrom(encoder.encode(commitments.get(k)))) + .setK(k) + .build(); + channel.broadcastMessage(createMessage(DKG.Payload.Type.COMMITMENT, commitmentMessage)); + } + } + + /** + * Send channel j her secret share (of my polynomial) + * @param j + */ + public void sendSecret(int j){ + ByteString secret = ByteString.copyFrom(getShare(j).y.toByteArray()); + channel.sendMessage(j, createMessage(DKG.Payload.Type.SHARE, + DKG.ShareMessage.newBuilder() + .setI(id) + .setJ(j) + .setShare(secret) + )); + } + + /** + * stage1.2 according to the protocol + * Pi computes the shares Sij for j = 1,...,n and sends Sij secretly to Pj. + */ + public void sendSecrets(){ + for (int j = 1; j <= n ; j++){ + if(j != id){ + sendSecret(j); + } + } + } + + /** + * + * @param i + * @return computeVerificationValue(j,parties[i - 1].commitments,group) == g ^ parties[i - 1].share mod q + */ + public boolean isValidShare(int i){ + Party party = parties[i - 1]; + synchronized (parties[i - 1]) { + return isValidShare(party.share, party.commitments, id); + } + } + + /** + * @param share + * @param commitments + * @param j + * @return computeVerificationValue(j,commitments,group) == g ^ secret.y mod q + */ + public boolean isValidShare(Polynomial.Point share, ArrayList commitments, int j){ + try{ + T v = computeVerificationValue(j,commitments,group); + return group.multiply(g,share.y).equals(v); + } + catch (NullPointerException e){ + return false; + } + } + + /** + * stage2 according to the protocol + * Pj verifies all the shares he received (using isValidShare) + * if check fails for an index i, Pj broadcasts a complaint against Pi. + */ + public void broadcastComplaints(){ + for (int i = 1; i <= n ; i++ ){ + if(i != id && !isValidShare(i)) { + broadcastComplaint(i); + } + } + } + + /** + * create a complaint message against i and broadcast it + * @param i + */ + private void broadcastComplaint(int i){ + //message = new Message(Type.Complaint, j) + DKG.IDMessage complaint = DKG.IDMessage.newBuilder() + .setId(i) + .build(); + channel.broadcastMessage(createMessage(DKG.Payload.Type.COMPLAINT, complaint)); + } + + /** + * create an answer message for j and broadcast it + * @param j + */ + public void broadcastComplaintAnswer(int j){ + channel.broadcastMessage(createMessage(DKG.Payload.Type.ANSWER, DKG.ShareMessage.newBuilder() + .setI(id) + .setJ(j) + .setShare(ByteString.copyFrom(getShare(j).y.toByteArray())))); + } + + /** + * stage3.1 according to the protocol + * if more than t players complain against a player Pi he is disqualified. + */ + public void answerAllComplainingPlayers(){ + ComplaintState[] complaints = parties[id - 1].complaints; + for (int i = 1; i <= n; i++) { + switch (complaints[i - 1]) { + case Waiting: + broadcastComplaintAnswer(i); + break; + default: + break; + } + } + + } + + /** + * stage3.2 according to the protocol + * if any of the revealed shares fails the verification test, player Pi is disqualified. + * set QUAL to be the set of non-disqualified players. + */ + public Set calcQUAL(){ + Set QUAL = new HashSet(); + boolean nonDisqualified; + int counter; + for (int i = 1; i <= n; i++) { + synchronized (parties[i - 1]) { + ComplaintState[] complaints = parties[i - 1].complaints; + nonDisqualified = true; + counter = 0; + for (int j = 1; j <= n; j++) { + switch (complaints[j - 1]) { + case OK: + break; + case NonDisqualified: + counter++; + break; + default: + nonDisqualified = false; + break; + } + if (!nonDisqualified) + break; + } + if (nonDisqualified && counter <= t) { + QUAL.add(i); + } + } + } + return QUAL; + } + + /** + * compute Y, the commitment to the final public key (includes only qualifying set) + * stage4.1 according to the protocol + * public value y is computed as y = multiplication of yi mod p for i in QUAL + */ + public T calcY(Set QUAL){ + T y = group.zero(); + for (int i : QUAL) { + synchronized (parties[i - 1]) { + y = group.add(y, parties[i - 1].commitments.get(0)); + } + } + return y; + } + + /** + * stage4.2 according to the protocol + * public verification values are computed as Ak = multiplication + * of Aik mod p for i in QUAL for k = 0,...,t + */ + public ArrayList calcCommitments(Set QUAL){ + ArrayList commitments = new ArrayList(t+1); + T value; + for (int k = 0; k <= t; k++){ + value = group.zero(); + for (int i : QUAL) { + synchronized (parties[i - 1]) { + value = group.add(value, parties[i - 1].commitments.get(k)); + } + } + commitments.add(k,value); + } + return commitments; + } + + /** + * stage4.3 according to the protocol + * Pj sets is share of the share as xj = sum of Sij mod q for i in QUAL + */ + public Polynomial.Point calcShare(Set QUAL){ + BigInteger xj = BigInteger.ZERO; + for (int i : QUAL) { + synchronized (parties[i - 1]) { + xj = xj.add(parties[i - 1].share.y); + } + } + return new Polynomial.Point(BigInteger.valueOf(id) , xj.mod(q)); + } + + /** + * decode commitment from arr + * @param arr + * @return + */ + public T decodeCommitment(byte[] arr){ + return encoder.decode(arr); + } + + /** + * getter + * @return id + */ + public int getId() { + return id; + } + + /** + * getter + * @return channel + */ + public Channel getChannel() { + return channel; + } + + + /** + * getter + * @return encoder + */ + public ByteEncoder getEncoder() { + return encoder; + } + +} diff --git a/distributed-key-generation/src/main/java/meerkat/crypto/dkg/feldman/User.java b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/feldman/User.java new file mode 100644 index 0000000..f7c4624 --- /dev/null +++ b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/feldman/User.java @@ -0,0 +1,588 @@ +package meerkat.crypto.dkg.feldman; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.TextFormat; +import meerkat.comm.Channel; +import meerkat.crypto.dkg.comm.MessageHandler; +import meerkat.crypto.secretsharing.shamir.Polynomial; +import com.google.protobuf.ByteString; +import meerkat.protobuf.Comm; +import meerkat.protobuf.DKG; +import org.factcenter.qilin.primitives.Group; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; + +import static meerkat.crypto.dkg.comm.MessageUtils.createMessage; + +/** + * Created by Tzlil on 3/14/2016. + * + * implementation of joint feldman protocol user. + * + * according to the protocol, each user run feldman verifiable secret sharing + * as a dealer. + * + * by the end of run(), each party in QUAL has his own share of the generated random key. + * this key can be recover by any subset of QUAL of size at least t + 1. + */ +public class User implements Runnable { + final Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * joint feldman protocol object + */ + protected final Protocol dkg; + + /** + * a generator of cyclic group of order q. + * the generated group is a subgroup of the given group. + * it must be chosen such that computing discrete logarithms is hard in this group. + */ + protected final T g; + + /** + * cyclic group contains g. + */ + protected final Group group; + + /** + * user id + */ + protected final int id; + + /** + * threshold + */ + protected final int t; + + /** + * number of shares + */ + protected final int n; + + + /** + * channel object + */ + protected final Channel channel; // + + /** + * All parties participating in key generation. + * parties[id-1] has my info. + */ + protected final Party[] parties; + + /** + * set of all non-disqualified parties + */ + protected Set QUAL; + + /** + * my own share of the generated random key. + */ + protected Polynomial.Point share; + + /** + * public verification values + */ + protected ArrayList commitments; + + /** + * public value, + * y = g ^ key + */ + protected T y; + + protected BlockingQueue receiveQueue; + + /** + * constructor + * @param dkg joint feldman protocol object + * @param channel channel object + */ + public User(Protocol dkg, Channel channel) { + this.dkg = dkg; + + this.g = dkg.getGenerator(); + this.group = dkg.getGroup(); + this.n = dkg.getN(); + this.t = dkg.getT(); + this.id = dkg.getId(); + + this.channel = channel; + dkg.setChannel(channel); + registerReceiverCallback(); + + this.parties = dkg.getParties(); + this.QUAL = null; + this.commitments = null; + this.share = null; + this.y = null; + + this.receiveQueue = new LinkedBlockingDeque<>(); + + } + + /** + * create MessageHandler and register it as ReceiverCallback + */ + protected void registerReceiverCallback() { + channel.registerReceiverCallback(new meerkat.crypto.dkg.comm.MessageHandler() { + @Override + public void handleMessage(Comm.BroadcastMessage envelope) throws InvalidProtocolBufferException { + receiveQueue.add(envelope); + } + }); + +// this.messageHandler = new MessageHandler(); +// channel.registerReceiverCallback(messageHandler); + } + + /** + * Wait for at least one message to arrive, then handle any messages currently in the queue + */ + protected void waitAndHandleReceivedMessages() { + Comm.BroadcastMessage msg = null; + while (!stop && msg == null) { + try { + msg = receiveQueue.take(); + } catch (InterruptedException e) { + // Possibly stop + } + } + while (!stop && msg != null) { + try { + handleMessage(msg); + } catch (InvalidProtocolBufferException e) { + logger.warn("Received invalid message: {}", TextFormat.printToString(msg)); + } + msg = receiveQueue.poll(); + } + } + + /** + * stage1 according to the protocol + * 1. Pi broadcasts Aik for k = 0,...,t. + * 2. Pi computes the shares Sij for j = 1,...,n and sends Sij secretly to Pj. + */ + protected void stage1() { + dkg.broadcastCommitments(); + dkg.sendSecrets(); + } + + /** + * Check if all shares and commitments have arrived from other parties + */ + protected boolean isStageOneCompleted() { + for (int i = 0 ; i < n ; i++) { + if (!parties[i].aborted) { + if (parties[i].share == null) + return false; + for (int k = 0 ; k <= t ; k++) { + if (parties[i].commitments.get(k) == null) + return false; + } + } + } + return true; + } + + protected void waitUntilStageOneCompleted() { + while (!stop && !isStageOneCompleted()) + waitAndHandleReceivedMessages(); + } + + protected boolean isStageTwoCompleted() { + for (int i = 0 ; i < n ; i++) { + if (!parties[i].aborted && !parties[i].doneFlag) + return false; + } + return true; + } + + /** + * stage2 according to the protocol + * Pj verifies all the shares he received + * if check fails for an index i, Pj broadcasts a complaint against Pi. + * Pj broadcasts done message at the end of this stage + */ + protected void stage2() { + dkg.broadcastComplaints(); + //broadcast done message after all complaints + channel.broadcastMessage(createMessage(DKG.Payload.Type.DONE)); + } + + /** + * wait until all other parties done complaining by receiving done message + */ + protected void waitUntilStageTwoCompleted(){ + while (!stop && !isStageTwoCompleted()) + waitAndHandleReceivedMessages(); + } + + + protected boolean haveReceivedAllStage3ComplaintAnswers() { + for (int i = 0; i < n; i++) { + if (parties[i].aborted) + continue; + for (int j = 0; j < n; j++) { + if (parties[i].complaints[j].equals(Protocol.ComplaintState.Waiting)) + return false; + + } + } + return true; + } + + /** + * stage3 according to the protocol + * 1. if more than t players complain against a player Pi he is disqualified. + * otherwise Pi broadcasts the share Sij for each complaining player Pj. + * 2. if any of the revealed shares fails the verification test, player Pi is disqualified. + * set QUAL to be the set of non-disqualified players. + */ + protected void stage3(){ + dkg.answerAllComplainingPlayers(); + + // wait until there is no complaint waiting for answer + while (!stop && !haveReceivedAllStage3ComplaintAnswers()) + waitAndHandleReceivedMessages(); + + this.QUAL = dkg.calcQUAL(); + } + + /** + * stage4 according to the protocol + * 1. public value y is computed as y = multiplication of yi mod p for i in QUAL + * 2. public verification values are computed as Ak = multiplication of Aik mod p for i in QUAL for k = 0,...,t + * 3. Pj sets is share of the secret as xj = sum of Sij mod q for i in QUAL + */ + protected void stage4(){ + this.y = dkg.calcY(QUAL); + this.commitments = dkg.calcCommitments(QUAL); + this.share = dkg.calcShare(QUAL); + } + + @Override + public void run() { + this.runThread = Thread.currentThread(); + // For debugging + String previousName = runThread.getName(); + runThread.setName(getClass().getName() +":" + getID()); + + try { + stage1(); + waitUntilStageOneCompleted(); + if (stop) return; + stage2(); + waitUntilStageTwoCompleted(); + if (stop) return; + stage3(); + if (stop) return; + stage4(); + } finally { + runThread.setName(previousName); + } + } + + /** + * current thread in the main loop + */ + protected Thread runThread; + + /** + * flag indicates if there was request to stop the current run of the protocol + */ + protected boolean stop = false; + + /** + * Request the current run loop to exit gracefully + */ + public void stop() { + try { + stop = true; + runThread.interrupt(); + }catch (Exception e){ + //do nothing + } + + } + + /** + * getter + * @return commitments + */ + public ArrayList getCommitments() { + return commitments; + } + + /** + * getter + * @return g + */ + public T getGenerator() { + return g; + } + + /** + * getter + * @return group + */ + public Group getGroup() { + return group; + } + + /** + * getter + * @return share + */ + public Polynomial.Point getShare() { + return share; + } + + /** + * getter + * @return id + */ + public int getID() { + return id; + } + + /** + * getter + * @return n + */ + public int getN() { + return n; + } + + /** + * getter + * @return t + */ + public int getT() { + return t; + } + + /** + * getter + * @return y + */ + public T getPublicValue() { + return y; + } + + /** + * getter + * @return QUAL + */ + public Set getQUAL() { + return QUAL; + } + + /** + * getter + * @return channel + */ + public Channel getChannel() { + return channel; + } + + /** + * commitment message is valid if: + * 1. it was received in broadcast chanel + * 2. the sender didn't sent this commitment before + */ + protected boolean isValidCommitmentMessage(int sender, boolean isBroadcast, DKG.CommitmentMessage commitmentMessage){ + int i = sender - 1; + int k = commitmentMessage.getK(); + return isBroadcast && parties[i].commitments.get(k) == null; + } + + /** + * secret message is valid if: + * 1. it was received in private chanel + * 2. the sender didn't sent secret message before + * 3. secret.i == i + * 4. secret.j == id + */ + protected boolean isValidSecretMessage(int sender, boolean isBroadcast, DKG.ShareMessage secretMessage){ + int i = secretMessage.getI(); + int j = secretMessage.getJ(); + if(sender != i || isBroadcast) + return false; + else + return parties[i - 1].share == null && j == id; + + } + + /** + * done message is valid if: + * 1. it was received in broadcast chanel + * 2. the sender didn't sent done message before + */ + protected boolean isValidDoneMessage(int sender, boolean isBroadcast){ + return isBroadcast && !parties[sender - 1].doneFlag; + } + + + /** + * complaint message is valid if: + * 1. it was received in broadcast chanel + * 2. the sender didn't complain against id before + */ + protected boolean isValidComplaintMessage(int sender, boolean isBroadcast, DKG.IDMessage complaintMessage){ + int i = sender; + int j = complaintMessage.getId(); + + assert(i > 0); + assert(j > 0); + assert(i <= parties.length); + assert(j <= parties[i-1].complaints.length); + + return isBroadcast && parties[i - 1].complaints[j - 1].equals( Protocol.ComplaintState.OK); + } + + /** + * answer message is valid if: + * 1. it was received in broadcast chanel + * 2. secret.i == i + * 3. 1 <= secret.j <= n + * 4. it is marked that j complained against i and i didn't received + */ + protected boolean isValidAnswerMessage(int sender, boolean isBroadcast, DKG.ShareMessage secretMessage){ + int i = secretMessage.getI(); + int j = secretMessage.getJ(); + if(sender != i || !isBroadcast) + return false; + else + return j >= 1 && j <= n && parties[i - 1].complaints[j - 1].equals(Protocol.ComplaintState.Waiting); + } + + + public void handleMessage(Comm.BroadcastMessage envelope) throws InvalidProtocolBufferException { + int sender = envelope.getSender(); + boolean isBroadcast = !envelope.getIsPrivate(); + DKG.Payload msg = DKG.Payload.parseFrom(envelope.getPayload()); + + logger.debug("handling Message: Dst={}, Src={}, [{}]", + envelope.getDestination(), envelope.getSender(), TextFormat.printToString(msg)); + + switch (msg.getType()) { + case COMMITMENT: + /** + * saves the commitment + */ + assert msg.getPayloadDataCase() == DKG.Payload.PayloadDataCase.COMMITMENT; + DKG.CommitmentMessage commitmentMessage = msg.getCommitment(); + if (isValidCommitmentMessage(sender, isBroadcast, commitmentMessage)) { + int i = sender - 1; + int k = commitmentMessage.getK(); + + parties[i].commitments.set(k, extractCommitment(commitmentMessage)); + } + break; + + + case SHARE: + /** + * saves the secret + */ + assert msg.getPayloadDataCase() == DKG.Payload.PayloadDataCase.SHARE; + DKG.ShareMessage secretMessage = msg.getShare(); + if(isValidSecretMessage(sender,isBroadcast,secretMessage)) { + int i = secretMessage.getI(); + Polynomial.Point secret = extractShare(id,secretMessage.getShare()); + parties[i - 1].share = secret; + } + break; + + case DONE: + + /** + * marks that the sender was finished sending all his complaints + */ + if(isValidDoneMessage(sender,isBroadcast)) { + parties[sender - 1].doneFlag = true; + } + break; + + case COMPLAINT: + /** + * marks that the sender was complained against id + */ + if (msg.getPayloadDataCase() != DKG.Payload.PayloadDataCase.ID) { + logger.error("User {} Expecting ID message, got from SRC={} msg {}", getID(), envelope.getSender(), TextFormat.printToString(msg)); + assert (msg.getPayloadDataCase() == DKG.Payload.PayloadDataCase.ID); + } + + DKG.IDMessage complaintMessage = msg.getId(); + if(isValidComplaintMessage(sender,isBroadcast,complaintMessage)){ + int i = sender; + int j = complaintMessage.getId(); + parties[j - 1].complaints[i - 1] = Protocol.ComplaintState.Waiting; + } + break; + case ANSWER: + /** + * if the secret is valid, marks the complaint as NonDisqualified + * else marks it as Disqualified + * in case that the complainer is id ( j == id ), saves the secret + */ + assert msg.getPayloadDataCase() == DKG.Payload.PayloadDataCase.SHARE; + secretMessage = msg.getShare(); + if(isValidAnswerMessage(sender,isBroadcast,secretMessage)) { + int i = secretMessage.getI(); + int j = secretMessage.getJ(); + Polynomial.Point secret = extractShare(j,secretMessage.getShare()); + if (dkg.isValidShare(secret, parties[i - 1].commitments, j)) { + parties[i - 1].complaints[j - 1] = Protocol.ComplaintState.NonDisqualified; + } else { + parties[i - 1].complaints[j - 1] = Protocol.ComplaintState.Disqualified; + } + if (j == id) { + parties[i - 1].share = secret; + } + } + break; + case ABORT: + /** + * marks that the sender was aborted + */ + parties[sender - 1].aborted = true; + break; + default: + logger.error("Bad message: SRC={}, DST={}, Payload={}", envelope.getSender(), envelope.getDestination(), TextFormat.printToString(msg)); + break; + + } + } + + /** + * extract share value from ByteString + * @param i + * @param share + * @return new Point (i,share) + */ + public Polynomial.Point extractShare(int i, ByteString share){ + BigInteger x = BigInteger.valueOf(i); + BigInteger y = new BigInteger(share.toByteArray()); + return new Polynomial.Point(x,y); + } + + /** + * + * @param commitmentMessage + * @return + */ + public T extractCommitment(DKG.CommitmentMessage commitmentMessage){ + return dkg.decodeCommitment(commitmentMessage.getCommitment().toByteArray()); + } +} diff --git a/distributed-key-generation/src/main/java/meerkat/crypto/dkg/gjkr/Party.java b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/gjkr/Party.java new file mode 100644 index 0000000..529b7be --- /dev/null +++ b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/gjkr/Party.java @@ -0,0 +1,28 @@ +package meerkat.crypto.dkg.gjkr; + +import meerkat.crypto.secretsharing.shamir.Polynomial; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +/** + * Created by Tzlil on 3/16/2016. + * + * an extension of DistributedKeyGenerationParty + * contains all relevant information on specific party during + * the run of the safe protocol + */ +public class Party extends meerkat.crypto.dkg.feldman.Party { + public Polynomial.Point shareT; + public boolean ysDoneFlag; + public ArrayList verifiableValues; + public Set recoverSharesSet; + public Party(int id, int n, int t) { + super(id, n, t); + this.shareT = null; + this.ysDoneFlag = false; + this.verifiableValues = new ArrayList(this.commitments); + this.recoverSharesSet = new HashSet(); + } +} diff --git a/distributed-key-generation/src/main/java/meerkat/crypto/dkg/gjkr/Protocol.java b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/gjkr/Protocol.java new file mode 100644 index 0000000..aad773c --- /dev/null +++ b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/gjkr/Protocol.java @@ -0,0 +1,165 @@ +package meerkat.crypto.dkg.gjkr; + +import meerkat.crypto.dkg.comm.MessageUtils; +import meerkat.crypto.secretsharing.feldman.VerifiableSecretSharing; +import meerkat.crypto.secretsharing.shamir.Polynomial; +import com.google.protobuf.ByteString; +import meerkat.protobuf.DKG; +import org.factcenter.qilin.primitives.Group; +import org.factcenter.qilin.util.ByteEncoder; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Random; +import java.util.Set; + +/** + * Created by Tzlil on 3/16/2016. + * TODO: comments + * TODO: put Channel (ChannelImpl) in constructor + */ +public class Protocol extends meerkat.crypto.dkg.feldman.Protocol { + + private VerifiableSecretSharing maskingShares; + private final T h; + private Party[] parties; + + public Protocol(int t, int n, BigInteger zi, Random random, BigInteger q, T g + , T h, Group group, int id, ByteEncoder byteEncoder) { + super(t, n, zi, random, q, g, group, id,byteEncoder); + this.h = h; + BigInteger r = new BigInteger(q.bitLength(),random).mod(q); + this.maskingShares = new VerifiableSecretSharing(t,n,r,random,q,h,group); + this.parties = new Party[n]; + for (int i = 1; i <= n ; i++){ + this.parties[i - 1] = new Party(i,n,t); + } + this.parties[id - 1].share = getShare(id); + this.parties[id - 1].shareT = maskingShares.getShare(id); + super.setParties(parties); + } + + protected Party[] getParties(){ + return parties; + } + + protected void setParties(Party[] parties) { + super.setParties(parties); + this.parties = parties; + } + + + @Override + public void sendSecret(int j) { + Polynomial.Point secret = getShare(j); + Polynomial.Point secretT = maskingShares.getShare(j); + DKG.ShareMessage doubleSecretMessage = createShareMessage(id,j,secret,secretT); + // TODO: Change SHARE to SHARE + channel.sendMessage(j, MessageUtils.createMessage(DKG.Payload.Type.SHARE, doubleSecretMessage)); + } + + + @Override + public boolean isValidShare(int i){ + Party party = parties[i - 1]; + return isValidShare(party.share, party.shareT, party.verifiableValues, id); + } + + /** + * test if share, shareT are valid with respect to verificationValues + * @param share + * @param shareT + * @param verificationValues + * @param j + * @return computeVerificationValue(j,verificationValues,group) == (g ^ share.y) * (h ^ shareT.y) mod q + */ + public boolean isValidShare(Polynomial.Point share, Polynomial.Point shareT, ArrayList verificationValues, int j){ + try { + T v = computeVerificationValue(j, verificationValues, group); + T exp = group.add(group.multiply(g, share.y), group.multiply(h, shareT.y)); + return exp.equals(v); + } + catch (NullPointerException e){ + return false; + } + } + + /** + * create complaint message against i and broadcast it + * @param share + * @param shareT + * @param i + */ + private void broadcastComplaint(Polynomial.Point share, Polynomial.Point shareT, int i){ + DKG.ShareMessage complaint = createShareMessage(i,id,share,shareT); + channel.broadcastMessage(MessageUtils.createMessage(DKG.Payload.Type.COMPLAINT, complaint)); + } + + /** + * stage4.3 according to the protocol + * if check fails for index i, Pj + */ + public void computeAndBroadcastComplaints(Set QUAL){ + Party party; + for (int i : QUAL) { + party = parties[i - 1]; + if (i != id) { + if (!super.isValidShare(party.share, party.commitments, id)) { + broadcastComplaint(party.share, party.shareT, i); + } + } + } + } + + /** + * compute verification values and broadcast them + * verificationValues[k] = g ^ commitments [k] * h ^ maskingShares.commitments [k] + */ + public void computeAndBroadcastVerificationValues(){ + ArrayList verificationValues = new ArrayList(t+1); + ArrayList hBaseCommitments = maskingShares.getCommitmentsArrayList(); + for (int k = 0 ; k <= t ; k++){ + verificationValues.add(k,group.add(commitmentsArrayList.get(k),hBaseCommitments.get(k))); + } + broadcastCommitments(verificationValues); + } + + /** + * pack share, shareT i,j to createShareMessage + * @param i + * @param j + * @param share + * @param shareT + * @return + */ + + private DKG.ShareMessage createShareMessage(int i, int j, Polynomial.Point share, Polynomial.Point shareT){ + DKG.ShareMessage ShareMessage = DKG.ShareMessage.newBuilder() + .setI(i) + .setJ(j) + .setShare(ByteString.copyFrom(share.y.toByteArray())) + .setShareT(ByteString.copyFrom(shareT.y.toByteArray())) + .build(); + return ShareMessage; + } + + @Override + public void broadcastComplaintAnswer(int j) { + DKG.ShareMessage answer = createShareMessage(id,j,getShare(j) + , maskingShares.getShare(j)); + channel.broadcastMessage(MessageUtils.createMessage(DKG.Payload.Type.ANSWER, answer)); + } + + public void broadcastAnswer(Polynomial.Point secret, Polynomial.Point secretT, int i){ + DKG.ShareMessage complaint = createShareMessage(i,id,secret,secretT); + channel.broadcastMessage(MessageUtils.createMessage(DKG.Payload.Type.ANSWER,complaint)); + } + + /** + * getter + * @return h + */ + public T getH() { + return h; + } +} diff --git a/distributed-key-generation/src/main/java/meerkat/crypto/dkg/gjkr/User.java b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/gjkr/User.java new file mode 100644 index 0000000..5c1b418 --- /dev/null +++ b/distributed-key-generation/src/main/java/meerkat/crypto/dkg/gjkr/User.java @@ -0,0 +1,359 @@ +package meerkat.crypto.dkg.gjkr; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.utils.Arithmetic; +import meerkat.crypto.utils.concrete.Fp; +import meerkat.comm.Channel; +import meerkat.crypto.secretsharing.shamir.Polynomial; +import meerkat.crypto.secretsharing.shamir.SecretSharing; +import meerkat.protobuf.Comm; +import meerkat.protobuf.DKG; + +import java.math.BigInteger; +import java.util.ArrayList; + +import static meerkat.crypto.dkg.comm.MessageUtils.*; + +/** + * Created by Tzlil on 3/16/2016. + *

+ * implementation of gjkr protocol user. + *

+ * this protocol extends joint Feldman protocol by splitting the protocol to commitment stage (stages 1,2,3) + * and revealing stage (stage 4). + *

+ * as in joint Feldman, each party in QUAL has his own share of the generated random key. + * this key can be recover by any subset of QUAL of size at least t + 1. + */ +public class User extends meerkat.crypto.dkg.feldman.User { + + /** + * All parties participating in key generation. + * parties[id-1] has my info. + */ + protected Party[] parties; + + /** + * gjkr secure protocol object + */ + protected final Protocol sdkg; + + boolean isStage4; + + /** + * constructor + * + * @param sdkg gjkr protocol object + * @param channel channel object + */ + public User(Protocol sdkg, Channel channel) { + super(sdkg, channel); + this.sdkg = sdkg; + this.parties = sdkg.getParties(); + } + + /** + * stage1 according to the protocol + * 1. Pi broadcasts Cik=Aik*Bik for k = 0,...,t. + * 2. Pi computes the shares Sij,Sij' for j = 1,...,n and sends Sij,Sij' secretly to Pj. + */ + @Override + protected void stage1() { + sdkg.computeAndBroadcastVerificationValues(); + sdkg.sendSecrets(); + } + + + + @Override + protected void waitUntilStageOneCompleted() { + super.waitUntilStageOneCompleted(); + // save the received commitments as verification values + ArrayList temp; + for (int i = 0; i < n; i++) { + temp = parties[i].verifiableValues; + parties[i].verifiableValues = parties[i].commitments; + parties[i].commitments = temp; + } + } + + /** + * stage2 according to the protocol + * Pj verifies all the shares,sharesT he received + * if check fails for an index i, Pj broadcasts a complaint against Pi. + * Pj broadcasts done message at the end of this stage + */ + @Override + protected void stage2() { + sdkg.broadcastComplaints(); + //broadcast done message after all complaints + channel.broadcastMessage(createMessage(DKG.Payload.Type.DONE)); + } + + /** + * Check if all non-aborting qualified parties have sent commitments. + * @return + */ + protected boolean haveAllQualPartiesCommitted() { + for (int i : QUAL) { + if (parties[i - 1].aborted) + continue; + for (int k = 0; k <= t; k++) { + if (parties[i - 1].commitments.get(k) == null) + return false; + } + } + return true; + } + + + /** + * Check if all non-aborting qualified parties sent a done message + * @return + */ + protected boolean areAllQualPartiesDone() { + for (int i : QUAL) { + if (parties[i - 1].aborted) + continue; + for (int k = 0; k <= t; k++) { + if (!parties[i - 1].ysDoneFlag) + return false; + } + } + return true; + } + + /** + * Check if at least t + 1 secrets were received foreach i in QUAL that aborted + * @return + */ + protected boolean haveReceivedEnoughSecretShares() { + for (int i : QUAL) { + if (parties[i - 1].aborted && parties[i - 1].recoverSharesSet.size() <= t) + return false; + } + return true; + } + + /** + * broadcast commitments and recover parties information if necessary + */ + private void resolveQualifyingPublicKey() { + sdkg.broadcastCommitments(); + // wait until all parties in QUAL broadcast their commitments or aborted + while (!stop && !haveAllQualPartiesCommitted()) + waitAndHandleReceivedMessages(); + + if (stop) + return; + + sdkg.computeAndBroadcastComplaints(QUAL); + + //broadcast done message after all complaints + channel.broadcastMessage(createMessage(DKG.Payload.Type.DONE)); + + // wait until all parties in QUAL done or aborted + while (!stop && !areAllQualPartiesDone()) + waitAndHandleReceivedMessages(); + + if (stop) + return; + + // broadcast i private secret foreach i in QUAL that aborted + for (int i : QUAL) { + if (parties[i - 1].aborted) { + sdkg.broadcastAnswer(parties[i - 1].share, parties[i - 1].shareT, i); + } + } + // wait until at least t + 1 secrets will received foreach i in QUAL that aborted + while (!stop && !haveReceivedEnoughSecretShares()) + waitAndHandleReceivedMessages(); + + if (stop) + return; + + Arithmetic arithmetic = new Fp(sdkg.getQ()); + // restore necessary information + for (int i = 0; i < n; i++) { + if (parties[i].recoverSharesSet.isEmpty()) { + continue; + } + Polynomial.Point[] shares = new Polynomial.Point[t + 1]; + int j = 0; + for (Polynomial.Point share : parties[i].recoverSharesSet) { + shares[j++] = share; + if (j >= shares.length) { + break; + } + } + Polynomial polynomial = SecretSharing.recoverPolynomial(shares, arithmetic); + BigInteger[] coefficients = polynomial.getCoefficients(); + for (int k = 0; k <= t; k++) { + parties[i].commitments.add(k, group.multiply(g, coefficients[k])); + } + parties[i].share = new Polynomial.Point(BigInteger.valueOf(id), polynomial); + } + } + + /** + * notifies message handler and message handler that stage 4 was started + */ + protected void setStage4() { + isStage4 = true; + } + + @Override + protected void stage4() { + setStage4(); + resolveQualifyingPublicKey(); + if (stop) return; + super.stage4(); + } + + + /** + * if !isStage4 as super, with extension to double secret message + * else answer message is valid if: + * 1. it was received in broadcast chanel + * 2. secret.j == sender + * 3. QUAL contains i and j + */ + protected boolean isValidAnswerMessage(int sender, boolean isBroadcast, DKG.ShareMessage doubleSecretMessage) { + if (!isStage4) { + return super.isValidAnswerMessage(sender, isBroadcast, doubleSecretMessage); + } else { + int i = doubleSecretMessage.getI(); + int j = doubleSecretMessage.getJ(); + return isBroadcast && j == sender && parties[i - 1].aborted && !parties[j - 1].aborted + && QUAL.contains(i) && QUAL.contains(j); + } + } + + + /** + * as in super with respect to protocol stage + */ + @Override + protected boolean isValidDoneMessage(int sender, boolean isBroadcast) { + if (!isStage4) { + return super.isValidDoneMessage(sender, isBroadcast); + } else { + return isBroadcast && !parties[sender - 1].ysDoneFlag; + } + } + + /** + * use only in stage4 + * complaint message is valid if: + * 1. it was received in broadcast chanel + * 2. secret.j == sender + * 3. QUAL contains i and j + */ + protected boolean isValidComplaintMessage(int sender, boolean isBroadcast, + DKG.ShareMessage complaintMessage) { + int i = complaintMessage.getI(); + int j = complaintMessage.getJ(); + return isBroadcast && j == sender && QUAL.contains(i) && QUAL.contains(j); + } + + + @Override + public void handleMessage(Comm.BroadcastMessage envelope) throws InvalidProtocolBufferException { + int sender = envelope.getSender(); + boolean isBroadcast = !envelope.getIsPrivate(); + DKG.Payload msg = DKG.Payload.parseFrom(envelope.getPayload()); + switch (msg.getType()) { + case SHARE: + /** + * as in super, with extension to double secret message + */ + DKG.ShareMessage doubleSecretMessage = msg.getShare(); + if (isValidSecretMessage(sender, isBroadcast, doubleSecretMessage)) { + int i = doubleSecretMessage.getI(); + synchronized (parties[i - 1]) { + parties[i - 1].share = extractShare(id, doubleSecretMessage.getShare()); + parties[i - 1].shareT = extractShare(id, doubleSecretMessage.getShareT()); + parties[i - 1].notify(); + } + } + break; + case ANSWER: + /** + * if !isStage4 as super, with extension to double secret message + * else saves secret + */ + assert msg.getPayloadDataCase() == DKG.Payload.PayloadDataCase.SHARE; + doubleSecretMessage = msg.getShare(); + if (isValidAnswerMessage(sender, isBroadcast, doubleSecretMessage)) { + int i = doubleSecretMessage.getI(); + int j = doubleSecretMessage.getJ(); + Polynomial.Point secret = extractShare(j, doubleSecretMessage.getShare()); + Polynomial.Point secretT = extractShare(j, doubleSecretMessage.getShareT()); + synchronized (parties[i - 1]) { + if (!isStage4) { + if (sdkg.isValidShare(secret, secretT, parties[j - 1].verifiableValues, i)) { + parties[i - 1].complaints[j - 1] = meerkat.crypto.dkg.feldman.Protocol.ComplaintState.NonDisqualified; + + } else { + parties[i - 1].complaints[j - 1] = meerkat.crypto.dkg.feldman.Protocol.ComplaintState.Disqualified; + } + if (j == id) { + parties[i - 1].share = secret; + parties[i - 1].shareT = secretT; + } + } else if (sdkg.isValidShare(secret, secretT, parties[i - 1].verifiableValues, j)) { + parties[i - 1].recoverSharesSet.add(secret); + } + parties[i - 1].notify(); + } + } + break; + case DONE: + /** + * as in super with respect to protocol state + */ + if (!isStage4) + super.handleMessage(envelope); + else { + if (isValidDoneMessage(sender, isBroadcast)) { + synchronized (parties[sender - 1]) { + parties[sender - 1].ysDoneFlag = true; + parties[sender - 1].notify(); + } + } + } + break; + case COMPLAINT: + /** + * if !isStage4 as in super + * else if secret,secretT are valid with respect to verifiableValues but + * secret is not valid with respect to commitments then + * marks i as aborted + */ + if (!isStage4) { + super.handleMessage(envelope); + } else { + assert (msg.getPayloadDataCase() == DKG.Payload.PayloadDataCase.SHARE); + DKG.ShareMessage ysComplaintMessage = msg.getShare(); + if (isValidComplaintMessage(sender, isBroadcast, ysComplaintMessage)) { + int i = ysComplaintMessage.getI(); + int j = ysComplaintMessage.getJ(); + Polynomial.Point secret = extractShare(i, ysComplaintMessage.getShare()); + Polynomial.Point secretT = extractShare(i, ysComplaintMessage.getShareT()); + if (sdkg.isValidShare(secret, secretT, parties[i - 1].verifiableValues, j) + && !dkg.isValidShare(secret, parties[i - 1].commitments, j)) { + synchronized (parties[i - 1]) { + parties[i - 1].aborted = true; + parties[i - 1].notify(); + } + } + } + } + break; + default: + super.handleMessage(envelope); + break; + } + } + +} \ No newline at end of file diff --git a/distributed-key-generation/src/main/java/meerkat/crypto/secretsharing/feldman/VerifiableSecretSharing.java b/distributed-key-generation/src/main/java/meerkat/crypto/secretsharing/feldman/VerifiableSecretSharing.java new file mode 100644 index 0000000..0c91431 --- /dev/null +++ b/distributed-key-generation/src/main/java/meerkat/crypto/secretsharing/feldman/VerifiableSecretSharing.java @@ -0,0 +1,117 @@ +package meerkat.crypto.secretsharing.feldman; + +import meerkat.crypto.secretsharing.shamir.Polynomial; +import meerkat.crypto.secretsharing.shamir.SecretSharing; +import org.factcenter.qilin.primitives.Group; + +import java.util.ArrayList; +import java.math.BigInteger; +import java.util.Random; + +/** + * Created by Tzlil on 1/27/2016. + * + * an implementation of Feldman's verifiable secret sharing scheme. + * + * allows trusted dealer to share a key x among n parties. + * + */ +public class VerifiableSecretSharing extends SecretSharing { + /** + * cyclic group contains g. + */ + protected final Group group; + /** + * a generator of cyclic group of order q. + * the generated group is a subgroup of the given group. + * it must be chosen such that computing discrete logarithms is hard in this group. + */ + protected final T g; + /** + * commitments to polynomial coefficients. + * commitments[k] = g ^ coefficients[k] (group operation) + */ + protected final ArrayList commitmentsArrayList; + + /** + * constructor + * @param q a large prime. + * @param t threshold. Any t+1 share holders can recover the secret, + * but any set of at most t share holders cannot + * @param n number of share holders + * @param zi secret, chosen from Zq + * @param random use for generate random polynomial + * @param group + * @param q a large prime dividing group order. + * @param g a generator of cyclic group of order q. + * the generated group is a subgroup of the given group. + * it must be chosen such that computing discrete logarithms is hard in this group. + */ + public VerifiableSecretSharing(int t, int n, BigInteger zi, Random random, BigInteger q, T g + , Group group) { + super(t, n, zi, random,q); + this.g = g; + this.group = group; + assert (this.group.contains(g)); + this.commitmentsArrayList = generateCommitments(); + } + + /** + * commitments[i] = g ^ polynomial.coefficients[i] + * @return commitments + */ + private ArrayList generateCommitments() { + + Polynomial polynomial = getPolynomial(); + BigInteger[] coefficients = polynomial.getCoefficients(); + ArrayList commitments = new ArrayList(t + 1); + for (int i = 0 ; i <= t;i++){ + commitments.add(i,group.multiply(g,coefficients[i])); + } + return commitments; + } + + /** + * Compute verification value (g^{share value}) using coefficient commitments sent by dealer and my share id. + * @param j my share holder id + * @param commitments commitments to polynomial coefficients of share (received from dealer) + * @param group + * + * @return product of Aik ^ (j ^ k) == g ^ polynomial(i) + */ + public static T computeVerificationValue(int j, ArrayList commitments, Group group) { + T v = group.zero(); + BigInteger power = BigInteger.ONE; + BigInteger J = BigInteger.valueOf(j); + for (int k = 0 ; k < commitments.size() ; k ++){ + v = group.add(v,group.multiply(commitments.get(k),power)); + power = power.multiply(J); + } + return v; + } + + /** + * getter + * @return generator of group + */ + public T getGenerator() { + return g; + } + + /** + * getter + * @return group + */ + public Group getGroup(){ + return group; + } + + /** + * getter + * @return commitmentsArrayList + */ + public ArrayList getCommitmentsArrayList() { + return commitmentsArrayList; + } + +} diff --git a/distributed-key-generation/src/main/java/meerkat/crypto/secretsharing/shamir/LagrangePolynomial.java b/distributed-key-generation/src/main/java/meerkat/crypto/secretsharing/shamir/LagrangePolynomial.java new file mode 100644 index 0000000..e12d65f --- /dev/null +++ b/distributed-key-generation/src/main/java/meerkat/crypto/secretsharing/shamir/LagrangePolynomial.java @@ -0,0 +1,66 @@ +package meerkat.crypto.secretsharing.shamir; + +import meerkat.crypto.utils.Arithmetic; + +import java.math.BigInteger; + +/** + * Created by Tzlil on 1/28/2016. + * + * container of lagrange polynomial + * + * Constructor is private (use {@link #lagrangePolynomials(Polynomial.Point[], Arithmetic)} to construct) + * + * l = (evaluate/divisor)* polynomial + * + * Note : image and divisor stored separately for avoiding lose of information by division + */ +class LagrangePolynomial{ + public final Polynomial polynomial; + public final BigInteger image; + public final BigInteger divisor; + + /** + * inner constructor, stores all given parameters + * @param polynomial + * @param image + * @param divisor + */ + private LagrangePolynomial(Polynomial polynomial, BigInteger image, BigInteger divisor) { + this.polynomial = polynomial; + this.image = image; + this.divisor = divisor; + } + + /** + * static method + * @param points array points s.t there are no couple of points that shares the same x value + * + * @return the lagrange polynomials that mach to given points. + * in case there exists i != j s.t points[i].x == points[j].x returns null. + */ + public static LagrangePolynomial[] lagrangePolynomials(Polynomial.Point[] points,Arithmetic arithmetic) { + Polynomial one = new Polynomial(new BigInteger[]{BigInteger.ONE},arithmetic); + LagrangePolynomial[] lagrangePolynomials = new LagrangePolynomial[points.length]; + Polynomial[] factors = new Polynomial[points.length]; + for (int i = 0 ; i < factors.length ; i++){ + factors[i] = new Polynomial(new BigInteger[]{points[i].x.negate(),BigInteger.ONE},arithmetic); // X - Xi + } + Polynomial product; + BigInteger divisor; + for(int i = 0; i < points.length; i ++) { + product = one; + divisor = BigInteger.ONE; + for (int j = 0; j < points.length; j++) { + if (i != j) { + divisor = arithmetic.mul(divisor,arithmetic.sub(points[i].x,points[j].x)); + product = product.mul(factors[j]); + } + } + if(divisor.equals(BigInteger.ZERO)) + return null; + lagrangePolynomials[i] = new LagrangePolynomial(product,points[i].y,divisor); + } + return lagrangePolynomials; + } +} diff --git a/distributed-key-generation/src/main/java/meerkat/crypto/secretsharing/shamir/Polynomial.java b/distributed-key-generation/src/main/java/meerkat/crypto/secretsharing/shamir/Polynomial.java new file mode 100644 index 0000000..b00aec5 --- /dev/null +++ b/distributed-key-generation/src/main/java/meerkat/crypto/secretsharing/shamir/Polynomial.java @@ -0,0 +1,209 @@ +package meerkat.crypto.secretsharing.shamir; + +import meerkat.crypto.utils.Arithmetic; + +import java.math.BigInteger; +import java.util.Arrays; + +/** + * Created by Tzlil on 1/27/2016. + */ +public class Polynomial implements Comparable { + private final int degree; + private final BigInteger[] coefficients; + private final Arithmetic arithmetic; + + /** + * constructor + * @param coefficients + * @param arithmetic + * degree set as max index such that coefficients[degree] not equals zero + */ + public Polynomial(BigInteger[] coefficients,Arithmetic arithmetic) { + int d = coefficients.length - 1; + while (d > 0 && coefficients[d].equals(BigInteger.ZERO)){ + d--; + } + this.degree = d; + this.coefficients = coefficients; + this.arithmetic = arithmetic; + } + + /** + * Compare to another polynomial (order by degree, then coefficients). + */ + @Override + public int compareTo(Polynomial other) { + if (this.degree != other.degree) + return this.degree - other.degree; + int compare; + for (int i = degree; i >= degree ; i--){ + compare = this.coefficients[i].compareTo(other.coefficients[i]); + if (compare != 0){ + return compare; + } + } + return 0; + } + + /** + * @param x + * @return sum of coefficients[i] * (x ^ i) + */ + public BigInteger evaluate(BigInteger x){ + BigInteger result = BigInteger.ZERO; + BigInteger power = BigInteger.ONE; + for(int i = 0 ; i <= degree ; i++){ + result = arithmetic.add(result,arithmetic.mul(coefficients[i],power)); + power = power.multiply(x); + } + return result; + } + + /** + * @param points + * @return polynomial of minimal degree which goes through all points. + * If there exists i != j s.t points[i].x == points[j].x, method returns null. + */ + public static Polynomial interpolation(Point[] points, Arithmetic arithmetic) { + LagrangePolynomial[] l = LagrangePolynomial.lagrangePolynomials(points,arithmetic); + if (l == null){ + return null; + } + // product = product of l[i].divisor + BigInteger product = BigInteger.ONE; + for (int i = 0; i < l.length;i++){ + product = arithmetic.mul(product,l[i].divisor); + } + + // factor[i] = product divided by l[i].divisor = product of l[j].divisor s.t j!=i + BigInteger[] factors = new BigInteger[l.length]; + for (int i = 0; i < l.length;i++){ + factors[i] = arithmetic.div(product,l[i].divisor); + } + int degree = l[0].polynomial.degree; + + // coefficients[j] = (sum of l[i].evaluate * factor[i] * l[i].coefficients[j] s.t i!=j) divide by product = + // = sum of l[i].evaluate * l[i].coefficients[j] / l[i].divisor s.t i!=j + BigInteger[] coefficients = new BigInteger[degree + 1]; + for (int j = 0; j < coefficients.length;j++){ + coefficients[j] = BigInteger.ZERO; + for (int i = 0; i < l.length; i++){ + BigInteger current = arithmetic.mul(l[i].image,factors[i]); + current = arithmetic.mul(current,l[i].polynomial.coefficients[j]); + coefficients[j] = arithmetic.add(coefficients[j],current); + } + coefficients[j] = arithmetic.div(coefficients[j],product); + } + return new Polynomial(coefficients,arithmetic); + } + + /** + * @param other + * @return new Polynomial of degree max(this degree,other degree) s.t for all x + * new.evaluate(x) = this.evaluate(x) + other.evaluate(x) + */ + public Polynomial add(Polynomial other){ + Polynomial bigger,smaller; + if(this.degree < other.degree){ + bigger = other; + smaller = this; + }else{ + bigger = this; + smaller = other; + } + BigInteger[] coefficients = bigger.getCoefficients(); + + for (int i = 0; i <= smaller.degree ; i++){ + coefficients[i] = arithmetic.add(smaller.coefficients[i],bigger.coefficients[i]); + } + return new Polynomial(coefficients,other.arithmetic); + } + + /** + * @param constant + * @return new Polynomial of degree this.degree s.t for all x + * new.evaluate(x) = constant * this.evaluate(x) + */ + public Polynomial mul(BigInteger constant){ + + BigInteger[] coefficients = this.getCoefficients(); + + for (int i = 0; i <= this.degree ; i++){ + coefficients[i] = arithmetic.mul(constant,coefficients[i]); + } + return new Polynomial(coefficients,arithmetic); + } + + /** + * @param other + * @return new Polynomial of degree this degree + other degree + 1 s.t for all x + * new.evaluate(x) = this.evaluate(x) * other.evaluate(x) + */ + public Polynomial mul(Polynomial other){ + + BigInteger[] coefficients = new BigInteger[this.degree + other.degree + 1]; + Arrays.fill(coefficients,BigInteger.ZERO); + + for (int i = 0; i <= this.degree ; i++){ + for (int j = 0; j <= other.degree; j++){ + coefficients[i+j] = arithmetic.add(coefficients[i+j],arithmetic.mul(this.coefficients[i],other.coefficients[j])); + } + } + return new Polynomial(coefficients,arithmetic); + } + + + /** getter + * @return copy of coefficients + */ + public BigInteger[] getCoefficients() { + return Arrays.copyOf(coefficients,coefficients.length); + } + + /** getter + * @return degree + */ + public int getDegree() { + return degree; + } + + /** + * inner class + * container for (x,y) x from range and y from evaluate of polynomial + */ + public static class Point implements java.io.Serializable { + public final BigInteger x; + public final BigInteger y; + + /** + * constructor + * @param x + * @param polynomial y = polynomial.evaluate(x) + */ + public Point(BigInteger x, Polynomial polynomial) { + this.x = x; + this.y = polynomial.evaluate(x); + } + + /** + * constructor + * @param x + * @param y + */ + public Point(BigInteger x,BigInteger y) { + this.x = x; + this.y = y; + } + + @Override + public boolean equals(Object obj) { + //TODO: is this implementation correct? cannot understand its logic (hai) + if(!super.equals(obj)) + return false; + Point other = (Point)obj; + return this.x.equals(other.x) && this.y.equals(other.y); + } + } + +} diff --git a/distributed-key-generation/src/main/java/meerkat/crypto/secretsharing/shamir/SecretSharing.java b/distributed-key-generation/src/main/java/meerkat/crypto/secretsharing/shamir/SecretSharing.java new file mode 100644 index 0000000..fb86993 --- /dev/null +++ b/distributed-key-generation/src/main/java/meerkat/crypto/secretsharing/shamir/SecretSharing.java @@ -0,0 +1,123 @@ +package meerkat.crypto.secretsharing.shamir; + +import meerkat.crypto.utils.Arithmetic; +import meerkat.crypto.utils.concrete.Fp; + +import java.math.BigInteger; +import java.util.Random; + +/** + * Created by Tzlil on 1/27/2016. + * an implementation of Shamire's secret sharing scheme + */ +public class SecretSharing{ + /** + * threshold + */ + protected final int t; + /** + * number of shares + */ + protected final int n; + /** + * a large prime + */ + protected final BigInteger q; + /** + * random polynomial of degree s.t polynomial.evaluate(0) = secret + */ + protected final Polynomial polynomial; + + /** + * constructor + * @param q a large prime. + * @param t threshold. Any t+1 share holders can recover the secret, + * but any set of at most t share holders cannot + * @param n number of share holders + * @param zi secret, chosen from Zq + * @param random use for generate random polynomial + */ + public SecretSharing(int t, int n, BigInteger zi, Random random, BigInteger q) { + this.q = q; + this.t = t; + this.n = n; + this.polynomial = generateRandomPolynomial(zi,random); + } + + /** + * @param x + * @param random + * @return new Polynomial polynomial of degree t ,such that + * 1. polynomial(0) = x + * 2. polynomial coefficients randomly chosen from Zq (except of coefficients[0] = x) + */ + private Polynomial generateRandomPolynomial(BigInteger x, Random random) { + BigInteger[] coefficients = new BigInteger[t + 1]; + coefficients[0] = x.mod(q); + int bits = q.bitLength(); + for (int i = 1 ; i <= t; i++ ){ + coefficients[i] = new BigInteger(bits,random).mod(q); + } + return new Polynomial(coefficients,new Fp(q)); + } + + /** + * @param i in range of [1,...n] + * + * @return polynomial.evaluate(i) + */ + public Polynomial.Point getShare(int i){ + assert (i > 0 && i <= n); + return new Polynomial.Point(BigInteger.valueOf(i), polynomial); + } + + /** + * @param shares - subset of the original shares + * + * @return evaluate of interpolation(shares) at x = 0 + */ + public static BigInteger recoverSecret(Polynomial.Point[] shares, Arithmetic arithmetic) { + return recoverPolynomial(shares,arithmetic).evaluate(BigInteger.ZERO); + } + /** + * @param shares - subset of the original shares + * + * @return interpolation(shares) + */ + public static Polynomial recoverPolynomial(Polynomial.Point[] shares, Arithmetic arithmetic) { + return Polynomial.interpolation(shares,arithmetic); + } + + /** + * getter + * @return threshold + */ + public int getT() { + return t; + } + + /** + * getter + * @return number of share holders + */ + public int getN() { + return n; + } + + /** + * getter + * @return the prime was given in the constructor + */ + public BigInteger getQ() { + return q; + } + + + /** + * getter + * @return the polynomial was generated in constructor + */ + public Polynomial getPolynomial() { + return polynomial; + } +} diff --git a/distributed-key-generation/src/main/java/meerkat/crypto/utils/Arithmetic.java b/distributed-key-generation/src/main/java/meerkat/crypto/utils/Arithmetic.java new file mode 100644 index 0000000..6221a5c --- /dev/null +++ b/distributed-key-generation/src/main/java/meerkat/crypto/utils/Arithmetic.java @@ -0,0 +1,38 @@ +package meerkat.crypto.utils; + +/** + * Created by Tzlil on 3/17/2016. + * defines the properties of the traditional operations : add,sub,mul,div + * between two objects of type T + */ +public interface Arithmetic { + /** + * addition + * @param a + * @param b + * @return a + b + */ + T add(T a, T b); + /** + * subtraction + * @param a + * @param b + * @return a - b + */ + T sub(T a, T b); + /** + * multiplication + * @param a + * @param b + * @return a * b + */ + T mul(T a, T b); + /** + * division + * @param a + * @param b + * @return a / b + */ + T div(T a, T b); + +} diff --git a/distributed-key-generation/src/main/java/meerkat/crypto/utils/concrete/Fp.java b/distributed-key-generation/src/main/java/meerkat/crypto/utils/concrete/Fp.java new file mode 100644 index 0000000..52fb324 --- /dev/null +++ b/distributed-key-generation/src/main/java/meerkat/crypto/utils/concrete/Fp.java @@ -0,0 +1,44 @@ +package meerkat.crypto.utils.concrete; + +import meerkat.crypto.utils.Arithmetic; +import org.factcenter.qilin.primitives.concrete.Zpstar; + +import java.math.BigInteger; + +/** + * Created by Tzlil on 3/17/2016. + * an implementation of Arithmetic over prime fields: integers modulo p + */ +public class Fp implements Arithmetic { + public final BigInteger p; + private final Zpstar zp; + + /** + * constructor + * @param p prime + */ + public Fp(BigInteger p) { + this.p = p; + this.zp = new Zpstar(p); + } + + @Override + public BigInteger add(BigInteger a, BigInteger b){ + return a.add(b).mod(p); + } + + @Override + public BigInteger sub(BigInteger a, BigInteger b){ + return a.add(p).subtract(b).mod(p); + } + + @Override + public BigInteger mul(BigInteger a, BigInteger b){ + return zp.add(a,b); + } + + @Override + public BigInteger div(BigInteger a, BigInteger b){ + return mul(a,zp.negate(b)); + } +} diff --git a/distributed-key-generation/src/main/proto/meerkat/DKG.proto b/distributed-key-generation/src/main/proto/meerkat/DKG.proto new file mode 100644 index 0000000..3457fbe --- /dev/null +++ b/distributed-key-generation/src/main/proto/meerkat/DKG.proto @@ -0,0 +1,46 @@ +syntax = "proto3"; + +package meerkat; + +option java_package = "meerkat.protobuf"; + +message Payload { + enum Type { + SHARE = 0; + COMMITMENT = 1; + COMPLAINT = 2; + DONE = 3; + ANSWER = 4; + YCOMMITMENT = 5; + YCOMPLAINT = 6; + YANSWER = 7; + ABORT = 8; + } + + + // Type of message in protocol + Type type = 1; + + oneof payload_data { + IDMessage id = 5; + ShareMessage share = 6; + CommitmentMessage commitment = 7; + } +} + +message IDMessage { + int32 id = 1; +} + +message ShareMessage { + int32 i = 1; + int32 j = 2; + bytes share = 3; + // For double shares (used in GJKR protocol) + bytes share_t = 4; +} + +message CommitmentMessage { + int32 k = 1; + bytes commitment = 2; +} diff --git a/distributed-key-generation/src/test/java/meerkat/crypto/dkg/feldman/DKGMaliciousUser.java b/distributed-key-generation/src/test/java/meerkat/crypto/dkg/feldman/DKGMaliciousUser.java new file mode 100644 index 0000000..78b70d4 --- /dev/null +++ b/distributed-key-generation/src/test/java/meerkat/crypto/dkg/feldman/DKGMaliciousUser.java @@ -0,0 +1,73 @@ +package meerkat.crypto.dkg.feldman; + +import meerkat.comm.Channel; + +import java.math.BigInteger; +import java.util.*; + +/** + * Created by Tzlil on 3/21/2016. + */ +public class DKGMaliciousUser extends User { + + private final Protocol maliciousDkg; + private final Set falls; + public DKGMaliciousUser(Protocol dkg, Protocol maliciousDKG, Channel channel, Set falls) { + super(dkg, channel); + this.falls = falls; + this.maliciousDkg = maliciousDKG; + maliciousDKG.setParties(parties); + } + + public static Set selectFallsRandomly(Set ids, Random random){ + Set falls = new HashSet(); + ArrayList idsList = new ArrayList(); + for (int id : ids){ + idsList.add(id); + } + int fallsSize = random.nextInt(idsList.size()) + 1;// 1 - (n-1) + while (falls.size() < fallsSize){ + falls.add(idsList.remove(random.nextInt(idsList.size()))); + } + return falls; + } + + public static Protocol generateMaliciousDKG(Protocol dkg,Channel channel,Random random){ + BigInteger q = dkg.getQ(); + BigInteger zi = new BigInteger(q.bitLength(), random).mod(q); + Protocol malicious = new Protocol(dkg.getT(),dkg.getN(),zi,random,dkg.getQ() + ,dkg.getGenerator(),dkg.getGroup(),dkg.getId(),dkg.getEncoder()); + malicious.setChannel(channel); + return malicious; + } + + @Override + public void stage1() { + dkg.broadcastCommitments(); + sendSecrets(); //insteadof crypto.sendSecrets(channel); + } + + @Override + public void stage3() { + maliciousDkg.answerAllComplainingPlayers(); + } + + @Override + public void stage4(){ + // do nothing + } + + private void sendSecrets(){ + for (int j = 1; j <= n ; j++){ + if(j != id){ + if(falls.contains(j)){ + maliciousDkg.sendSecret(j); + }else { + dkg.sendSecret(j); + } + } + } + } + + +} diff --git a/distributed-key-generation/src/test/java/meerkat/crypto/dkg/feldman/DKGTest.java b/distributed-key-generation/src/test/java/meerkat/crypto/dkg/feldman/DKGTest.java new file mode 100644 index 0000000..f522a42 --- /dev/null +++ b/distributed-key-generation/src/test/java/meerkat/crypto/dkg/feldman/DKGTest.java @@ -0,0 +1,170 @@ +package meerkat.crypto.dkg.feldman; + +import meerkat.comm.ChannelImpl; +import meerkat.crypto.utils.Arithmetic; +import meerkat.crypto.utils.concrete.Fp; +import meerkat.comm.Channel; +import meerkat.crypto.secretsharing.feldman.VerifiableSecretSharing; +import meerkat.crypto.secretsharing.shamir.Polynomial; +import meerkat.crypto.secretsharing.shamir.SecretSharing; +import meerkat.crypto.utils.BigIntegerByteEncoder; +import meerkat.crypto.utils.GenerateRandomPrime; +import org.factcenter.qilin.primitives.Group; +import org.factcenter.qilin.primitives.concrete.Zpstar; +import org.factcenter.qilin.util.ByteEncoder; +import org.junit.Test; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; + +/** + * Created by Tzlil on 3/21/2016. + */ +public class DKGTest { + + int tests = 10; + BigInteger p = GenerateRandomPrime.SafePrime100Bits; + BigInteger q = p.subtract(BigInteger.ONE).divide(BigInteger.valueOf(2)); + Group group = new Zpstar(p); + Arithmetic arithmetic = new Fp(q); + int t = 9; + int n = 20; + + public void oneTest(Testable testable) throws Exception { + for (int i = 0; i < testable.threads.length ; i++){ + testable.threads[i].start(); + } + for (int i = 0; i < testable.threads.length ; i++){ + testable.threads[i].join(); + } + + // got the right public value + BigInteger publicValue = group.multiply(testable.g,testable.secret); + for (int i: testable.valids){ + assert (testable.dkgs[i - 1].getPublicValue().equals(publicValue)); + } + + // assert valid verification values + BigInteger expected,verification; + for (int i: testable.valids){ + expected = group.multiply(testable.g, testable.dkgs[i - 1].getShare().y); + verification = VerifiableSecretSharing.computeVerificationValue(i, testable.dkgs[i - 1].getCommitments(), group); + assert (expected.equals(verification)); + } + + + // restore the secret from shares + ArrayList sharesList = new ArrayList(); + + for (int i: testable.valids){ + sharesList.add(testable.dkgs[i - 1].getShare()); + } + Polynomial.Point[] shares = new Polynomial.Point[sharesList.size()]; + for (int i = 0; i < shares.length; i ++){ + shares[i] = sharesList.get(i); + } + + BigInteger calculatedSecret = SecretSharing.recoverSecret(shares,arithmetic); + assert (calculatedSecret.equals(testable.secret)); + } + + + @Test + public void test() throws Exception { + Testable testable; + for (int i = 0; i < tests; i++){ + testable = new Testable(new Random()); + oneTest(testable); + } + } + + class Testable{ + Set valids; + Set QUAL; + Set aborted; + Set malicious; + User[] dkgs; + Thread[] threads; + BigInteger g; + BigInteger secret; + + public Testable(Random random) { + this.dkgs = new User[n]; + this.valids = new HashSet(); + this.QUAL = new HashSet(); + this.aborted = new HashSet(); + this.malicious = new HashSet(); + this.threads = new Thread[n]; + this.g = sampleGenerator(random); + ArrayList ids = new ArrayList(); + for (int id = 1; id<= n ; id++){ + ids.add(id); + } + int id; + BigInteger s; + Protocol dkg; + this.secret = BigInteger.ZERO; + ChannelImpl channels = new ChannelImpl(); + ByteEncoder byteEncoder = new BigIntegerByteEncoder(); + while (!ids.isEmpty()) { + id = ids.remove(random.nextInt(ids.size())); + Channel channel = channels.getChannel(id); + s = randomIntModQ(random); + dkg = new meerkat.crypto.dkg.feldman.Protocol(t, n, s, random, q, g, group, id,byteEncoder); + dkgs[id - 1] = randomDKGUser(id,channel,dkg,random); + threads[id - 1] = new Thread(dkgs[id - 1]); + if(QUAL.contains(id)){ + this.secret = this.secret.add(s).mod(q); + } + } + + } + + public User randomDKGUser(int id, Channel channel, Protocol dkg, Random random){ + if (QUAL.size() <= t) { + valids.add(id); + QUAL.add(id); + return new User(dkg,channel); + }else{ + int type = random.nextInt(3); + switch (type){ + case 0:// regular + valids.add(id); + QUAL.add(id); + return new User(dkg,channel); + case 1:// abort + int abortStage = random.nextInt(2) + 1; // 1 or 2 + aborted.add(id); + if (abortStage == 2){ + QUAL.add(id); + } + return new DKGUserImplAbort(dkg,channel,abortStage); + case 2:// malicious + malicious.add(id); + Set falls = DKGMaliciousUser.selectFallsRandomly(valids,random); + Protocol maliciousDKG = DKGMaliciousUser.generateMaliciousDKG(dkg,channel,random); + return new DKGMaliciousUser(dkg,maliciousDKG,channel,falls); + default: + return null; + } + } + } + + public BigInteger sampleGenerator(Random random){ + BigInteger ZERO = group.zero(); + BigInteger g; + do { + g = group.sample(random); + } while (!g.equals(ZERO) && !group.multiply(g, q).equals(ZERO)); + return g; + } + + public BigInteger randomIntModQ(Random random){ + return new BigInteger(q.bitLength(), random).mod(q); + } + + } +} diff --git a/distributed-key-generation/src/test/java/meerkat/crypto/dkg/feldman/DKGUserImplAbort.java b/distributed-key-generation/src/test/java/meerkat/crypto/dkg/feldman/DKGUserImplAbort.java new file mode 100644 index 0000000..528ea9f --- /dev/null +++ b/distributed-key-generation/src/test/java/meerkat/crypto/dkg/feldman/DKGUserImplAbort.java @@ -0,0 +1,65 @@ +package meerkat.crypto.dkg.feldman; + +import meerkat.comm.Channel; +import meerkat.protobuf.DKG; + +import static meerkat.crypto.dkg.comm.MessageUtils.createMessage; + +/** + * Created by Tzlil on 3/14/2016. + */ +public class DKGUserImplAbort extends User { + + final int abortStage; + int stage; + public DKGUserImplAbort(Protocol dkg, Channel channel, int abortStage) { + super(dkg, channel); + this.abortStage = abortStage;// 1 - 2 + this.stage = 1; + } + + + private void sendAbort(){ + channel.broadcastMessage(createMessage(DKG.Payload.Type.ABORT)); + } + + @Override + protected void stage1() { + if(stage < abortStage) + super.stage1(); + else if(stage == abortStage){ + sendAbort(); + } + stage++; + } + + @Override + protected void stage2() { + if(stage < abortStage) + super.stage2(); + else if(stage == abortStage){ + sendAbort(); + } + stage++; + } + + @Override + protected void stage3() { + if(stage < abortStage) + super.stage3(); + else if(stage == abortStage){ + sendAbort(); + } + stage++; + } + + @Override + protected void stage4() { + if(stage < abortStage) + super.stage4(); + else if(stage == abortStage){ + sendAbort(); + } + stage++; + } +} diff --git a/distributed-key-generation/src/test/java/meerkat/crypto/dkg/gjkr/SDKGMaliciousUserImpl.java b/distributed-key-generation/src/test/java/meerkat/crypto/dkg/gjkr/SDKGMaliciousUserImpl.java new file mode 100644 index 0000000..baf1a81 --- /dev/null +++ b/distributed-key-generation/src/test/java/meerkat/crypto/dkg/gjkr/SDKGMaliciousUserImpl.java @@ -0,0 +1,61 @@ +package meerkat.crypto.dkg.gjkr; + +import meerkat.comm.Channel; + +import java.math.BigInteger; +import java.util.Random; +import java.util.Set; + +/** + * Created by Tzlil on 3/29/2016. + */ +public class SDKGMaliciousUserImpl extends User { + + private final Protocol maliciousSDKG; + private final Set falls; + public SDKGMaliciousUserImpl(Protocol sdkg, Protocol maliciousSDKG + , Channel channel, Set falls) { + super(sdkg, channel); + this.falls = falls; + this.maliciousSDKG = maliciousSDKG; + maliciousSDKG.setParties(parties); + } + + public static Protocol generateMaliciousSDKG(Protocol sdkg,Channel channel,Random random){ + BigInteger q = sdkg.getQ(); + BigInteger zi = new BigInteger(q.bitLength(), random).mod(q); + Protocol malicious = new Protocol(sdkg.getT(),sdkg.getN(),zi,random,sdkg.getQ() + ,sdkg.getGenerator(),sdkg.getH(),sdkg.getGroup(),sdkg.getId(),sdkg.getEncoder()); + malicious.setChannel(channel); + return malicious; + } + + @Override + public void stage1() { + sdkg.computeAndBroadcastVerificationValues(); + sendSecrets(); //insteadof crypto.sendSecrets(channel); + } + + @Override + public void stage3() { + maliciousSDKG.answerAllComplainingPlayers(); + } + + @Override + public void stage4(){ + //do nothing + } + + private void sendSecrets(){ + for (int j = 1; j <= n ; j++){ + if(j != id){ + if(falls.contains(j)){ + maliciousSDKG.sendSecret(j); + }else { + sdkg.sendSecret(j); + } + } + } + } + +} diff --git a/distributed-key-generation/src/test/java/meerkat/crypto/dkg/gjkr/SDKGTest.java b/distributed-key-generation/src/test/java/meerkat/crypto/dkg/gjkr/SDKGTest.java new file mode 100644 index 0000000..d172c23 --- /dev/null +++ b/distributed-key-generation/src/test/java/meerkat/crypto/dkg/gjkr/SDKGTest.java @@ -0,0 +1,205 @@ +package meerkat.crypto.dkg.gjkr; + +import meerkat.comm.ChannelImpl; +import meerkat.crypto.utils.Arithmetic; +import meerkat.crypto.utils.concrete.Fp; +import meerkat.comm.Channel; +import meerkat.crypto.secretsharing.feldman.VerifiableSecretSharing; +import meerkat.crypto.dkg.feldman.DKGMaliciousUser; +import meerkat.crypto.secretsharing.shamir.Polynomial; +import meerkat.crypto.secretsharing.shamir.SecretSharing; +import meerkat.crypto.utils.BigIntegerByteEncoder; +import meerkat.crypto.utils.GenerateRandomPrime; +import meerkat.protobuf.Crypto; +import org.factcenter.qilin.primitives.Group; +import org.factcenter.qilin.primitives.concrete.Zpstar; +import org.factcenter.qilin.util.ByteEncoder; +import org.junit.Assert; +import org.junit.Test; +import org.junit.internal.runners.statements.Fail; + +import static org.junit.Assert.*; + +import java.math.BigInteger; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +/** + * Created by Tzlil on 3/29/2016. + * TODO: Separate into multiple tests, + * TODO: Make tests deterministic (using constant seed for random generator) + */ +public class SDKGTest { + + private ExecutorService executorService = Executors.newCachedThreadPool(); + + final static int NUM_TESTS = 10; + BigInteger p = GenerateRandomPrime.SafePrime100Bits; + BigInteger q = p.subtract(BigInteger.ONE).divide(BigInteger.valueOf(2)); + Group group = new Zpstar(p); + Arithmetic arithmetic = new Fp(q); + int initialT = 17; + int initialN = 20; + Random rand = new Random(1); + + public void oneTest(Testable testable) throws Exception { + for (int i = 0; i < testable.sdkgs.length; i++){ + testable.futures[i] = executorService.submit(testable.sdkgs[i]); + } + for (Future ftr : testable.futures) { + ftr.get(); + } + + // got the right public value + BigInteger publicValue = group.multiply(testable.g, testable.secret); + for (int i : testable.valids){ + assertEquals (testable.sdkgs[i-1].getPublicValue(), publicValue); + } + + // assert valid verification values + BigInteger expected,verification; + for (int i : testable.valids){ + expected = group.multiply(testable.g, testable.sdkgs[i - 1].getShare().y); + verification = VerifiableSecretSharing.computeVerificationValue(i, testable.sdkgs[i - 1].getCommitments(), group); + assertEquals (expected, verification); + } + + + // restore the secret from shares + ArrayList sharesList = new ArrayList<>(); + + for (int i : testable.valids){ + sharesList.add(testable.sdkgs[i - 1].getShare()); + } + Polynomial.Point[] shares = new Polynomial.Point[sharesList.size()]; + shares = sharesList.toArray(shares); + + BigInteger calculatedSecret = SecretSharing.recoverSecret(shares, arithmetic); + assertEquals (calculatedSecret, testable.secret); + } + + + @Test + public void runSharingProtocol() throws Exception { + Testable testable; + for (int i = 0; i < NUM_TESTS; i++) { + testable = new Testable(initialN+i, initialT+i, group, q, rand); + oneTest(testable); + } + } + + + static class Testable { + Set valids; + Set QUAL; + Set aborted; + Set malicious; + User[] sdkgs; + Future[] futures; + BigInteger g; + BigInteger h; + BigInteger secret; + Group group; + int n; + int t; + BigInteger q; + Random random; + ChannelImpl channels = new ChannelImpl(); + + public Testable(int n, int t, Group group, BigInteger q, Random random) { + this.n = n; + this.t = t; + this.group = group; + this.q = q; + this.random = random; + this.sdkgs = new User[n]; + this.valids = new HashSet<>(); + this.QUAL = new HashSet<>(); + this.aborted = new HashSet<>(); + this.malicious = new HashSet<>(); + this.futures = new Future[n]; + this.g = sampleGenerator(random); + this.h = group.multiply(g, randomIntModQ(random)); + List ids = new ArrayList<>(); + for (int id = 1; id<= n ; id++){ + ids.add(id); + } + int id; + BigInteger s; + Channel channel; + Protocol sdkg; + this.secret = BigInteger.ZERO; + ByteEncoder encoder = new BigIntegerByteEncoder(); + while (!ids.isEmpty()) { + id = ids.remove(random.nextInt(ids.size())); + s = randomIntModQ(random); + channel = channels.getChannel(id); + sdkg = new Protocol<>(t, n, s, random, q, g , h, group, id, encoder); + sdkgs[id - 1] = randomSDKGUser(id, channel, sdkg); + if(QUAL.contains(id)){ + this.secret = this.secret.add(s).mod(q); + } + } + + } + + enum UserType { + HONEST, + FAILSTOP, + MALICIOUS, + } + + public User newSDKGUser(int id, Channel channel, Protocol sdkg, UserType userType) { + switch(userType) { + case HONEST: + valids.add(id); + QUAL.add(id); + return new User<>(sdkg,channel); + + case FAILSTOP: + int abortStage = random.nextInt(3) + 1; // 1 or 2 or 3 + aborted.add(id); + if (abortStage > 1){ + QUAL.add(id); + } + return new SDKGUserImplAbort(sdkg, channel, abortStage); + + case MALICIOUS: + malicious.add(id); + Set falls = DKGMaliciousUser.selectFallsRandomly(valids, random); + Protocol maliciousSDKG = SDKGMaliciousUserImpl.generateMaliciousSDKG(sdkg, channel, random); + return new SDKGMaliciousUserImpl(sdkg, maliciousSDKG, channel, falls); + + } + fail("Unknown user type"); + return null; + } + + public User randomSDKGUser(int id, Channel channel, Protocol sdkg){ + if (QUAL.size() <= t) { + return newSDKGUser(id, channel, sdkg, UserType.HONEST); + } else { + UserType type = UserType.values()[random.nextInt(UserType.values().length)]; + return newSDKGUser(id, channel, sdkg, type); + } + } + + public BigInteger sampleGenerator(Random random){ + BigInteger ZERO = group.zero(); + BigInteger g; + do { + g = group.sample(random); + } while (!g.equals(ZERO) && !group.multiply(g, q).equals(ZERO)); + return g; + } + + public BigInteger randomIntModQ(Random random){ + return new BigInteger(q.bitLength(), random).mod(q); + } + + } + + +} diff --git a/distributed-key-generation/src/test/java/meerkat/crypto/dkg/gjkr/SDKGUserImplAbort.java b/distributed-key-generation/src/test/java/meerkat/crypto/dkg/gjkr/SDKGUserImplAbort.java new file mode 100644 index 0000000..5b0e11b --- /dev/null +++ b/distributed-key-generation/src/test/java/meerkat/crypto/dkg/gjkr/SDKGUserImplAbort.java @@ -0,0 +1,80 @@ +package meerkat.crypto.dkg.gjkr; + +import com.google.protobuf.InvalidProtocolBufferException; +import meerkat.crypto.dkg.comm.MessageUtils; +import meerkat.comm.Channel; +import meerkat.protobuf.Comm; +import meerkat.protobuf.DKG; + +/** + * Created by Tzlil on 3/14/2016. + */ +public class SDKGUserImplAbort extends User { + + public static class AbortException extends RuntimeException { + + } + + final int abortStage; + int stage; + public SDKGUserImplAbort(Protocol sdkg, Channel channel, int abortStage) { + super(sdkg, channel); + this.abortStage = abortStage;// 1 - 4 + this.stage = 1; + } + + private void abort(){ + //stopReceiver(); + channel.broadcastMessage(MessageUtils.createMessage(DKG.Payload.Type.ABORT)); + throw new AbortException(); + } + + @Override + protected void stage1() { + if(stage < abortStage) + super.stage1(); + else if(stage == abortStage){ + abort(); + } + stage++; + } + + @Override + protected void stage2() { + if(stage < abortStage) + super.stage2(); + else if(stage == abortStage){ + abort(); + } + stage++; + } + + @Override + protected void stage3() { + if(stage < abortStage) + super.stage3(); + else if(stage == abortStage){ + abort(); + } + stage++; + } + + @Override + protected void stage4() { + if(stage < abortStage) + super.stage4(); + else if(stage == abortStage){ + abort(); + } + stage++; + } + + @Override + public void run() { + try { + super.run(); + } catch (AbortException e) { + // Expected + } + } +} diff --git a/distributed-key-generation/src/test/java/meerkat/crypto/secretsharing/feldman/VerifiableSecretSharingTest.java b/distributed-key-generation/src/test/java/meerkat/crypto/secretsharing/feldman/VerifiableSecretSharingTest.java new file mode 100644 index 0000000..7992dfb --- /dev/null +++ b/distributed-key-generation/src/test/java/meerkat/crypto/secretsharing/feldman/VerifiableSecretSharingTest.java @@ -0,0 +1,68 @@ +package meerkat.crypto.secretsharing.feldman; + +import meerkat.crypto.secretsharing.shamir.Polynomial; +import org.factcenter.qilin.primitives.Group; +import org.factcenter.qilin.primitives.concrete.Zpstar; +import org.junit.Before; +import org.junit.Test; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Random; + +/** + * Created by Tzlil on 1/29/2016. + */ +public class VerifiableSecretSharingTest { + + + VerifiableSecretSharing[] verifiableSecretSharingArray; + int tests = 1 << 10; + Random random; + + @Before + public void settings(){ + BigInteger p = BigInteger.valueOf(2903); + BigInteger q = p.subtract(BigInteger.ONE).divide(BigInteger.valueOf(2)); + Zpstar zpstar = new Zpstar(p); + random = new Random(); + BigInteger g; + BigInteger ZERO = zpstar.zero(); + do{ + g = zpstar.sample(random); + }while (!g.equals(ZERO) && !zpstar.multiply(g,q).equals(ZERO));// sample from QRZp* + int t = 8; + int n = 20; + verifiableSecretSharingArray = new VerifiableSecretSharing[tests]; + for (int i = 0; i < verifiableSecretSharingArray.length; i++){ + verifiableSecretSharingArray[i] = new VerifiableSecretSharing(t,n + ,new BigInteger(q.bitLength(),random).mod(q),random,q,g,zpstar); + } + } + + public void oneTest(VerifiableSecretSharing verifiableSecretSharing) throws Exception { + int n = verifiableSecretSharing.getN(); + Group zpstar = verifiableSecretSharing.getGroup(); + BigInteger g = verifiableSecretSharing.getGenerator(); + Polynomial.Point[] shares = new Polynomial.Point[n]; + ArrayList commitments = verifiableSecretSharing.getCommitmentsArrayList(); + BigInteger[] verifications = new BigInteger[n]; + for (int i = 1 ; i <= shares.length; i ++){ + shares[i - 1] = verifiableSecretSharing.getShare(i); + verifications[i - 1] = VerifiableSecretSharing.computeVerificationValue(i,commitments,zpstar); + } + BigInteger expected; + for (int i = 0 ; i < shares.length ; i++){ + expected = zpstar.multiply(g,shares[i].y); + assert (expected.equals(verifications[i])); + } + + } + + @Test + public void secretSharingTest() throws Exception { + for (int i = 0 ; i < verifiableSecretSharingArray.length; i ++){ + oneTest(verifiableSecretSharingArray[i]); + } + } +} diff --git a/distributed-key-generation/src/test/java/meerkat/crypto/secretsharing/shamir/PolynomialTests/AddTest.java b/distributed-key-generation/src/test/java/meerkat/crypto/secretsharing/shamir/PolynomialTests/AddTest.java new file mode 100644 index 0000000..79a9aef --- /dev/null +++ b/distributed-key-generation/src/test/java/meerkat/crypto/secretsharing/shamir/PolynomialTests/AddTest.java @@ -0,0 +1,46 @@ +package meerkat.crypto.secretsharing.shamir.PolynomialTests; +import meerkat.crypto.utils.GenerateRandomPolynomial; +import meerkat.crypto.utils.Z; +import meerkat.crypto.secretsharing.shamir.Polynomial; +import org.junit.Before; +import org.junit.Test; + +import java.math.BigInteger; +import java.util.Random; + +/** + * Created by Tzlil on 1/27/2016. + */ +public class AddTest { + + Polynomial[] arr1; + Polynomial[] arr2; + int tests = 1 << 12; + int maxDegree = 15; + int bits = 128; + Random random; + + @Before + public void settings(){ + random = new Random(); + arr1 = new Polynomial[tests]; + arr2 = new Polynomial[tests]; + for (int i = 0; i < arr1.length; i++){ + arr1[i] = GenerateRandomPolynomial.generateRandomPolynomial(random.nextInt(maxDegree),bits,random,new Z()); + arr2[i] = GenerateRandomPolynomial.generateRandomPolynomial(random.nextInt(maxDegree),bits,random,new Z()); + } + } + + public void oneTest(Polynomial p1, Polynomial p2){ + Polynomial sum = p1.add(p2); + BigInteger x = new BigInteger(bits,random); + assert(sum.evaluate(x).equals(p1.evaluate(x).add(p2.evaluate(x)))); + } + + @Test + public void addTest(){ + for (int i = 0 ; i < arr1.length; i ++){ + oneTest(arr1[i],arr2[i]); + } + } +} diff --git a/distributed-key-generation/src/test/java/meerkat/crypto/secretsharing/shamir/PolynomialTests/InterpolationTest.java b/distributed-key-generation/src/test/java/meerkat/crypto/secretsharing/shamir/PolynomialTests/InterpolationTest.java new file mode 100644 index 0000000..ea857f8 --- /dev/null +++ b/distributed-key-generation/src/test/java/meerkat/crypto/secretsharing/shamir/PolynomialTests/InterpolationTest.java @@ -0,0 +1,68 @@ +package meerkat.crypto.secretsharing.shamir.PolynomialTests; + +import meerkat.crypto.secretsharing.shamir.Polynomial; +import meerkat.crypto.utils.Arithmetic; +import meerkat.crypto.utils.concrete.Fp; +import meerkat.crypto.utils.GenerateRandomPolynomial; +import meerkat.crypto.utils.GenerateRandomPrime; +import org.junit.Before; +import org.junit.Test; + +import java.math.BigInteger; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; + +/** + * Created by Tzlil on 1/27/2016. + */ +public class InterpolationTest { + Polynomial[] polynomials; + int tests = 1 << 10; + int maxDegree = 15; + int bits = 128; + Random random; + Polynomial.Point[][] pointsArrays; + Arithmetic arithmetic; + BigInteger p = GenerateRandomPrime.SafePrime100Bits; + + @Before + public void settings(){ + random = new Random(); + polynomials = new Polynomial[tests]; + pointsArrays = new Polynomial.Point[tests][]; + arithmetic = new Fp(p); + for (int i = 0; i < polynomials.length; i++){ + polynomials[i] = GenerateRandomPolynomial.generateRandomPolynomial(random.nextInt(maxDegree),bits,random,p); + pointsArrays[i] = randomPoints(polynomials[i]); + } + } + + public Polynomial.Point[] randomPoints(Polynomial polynomial){ + Polynomial.Point[] points = new Polynomial.Point[polynomial.getDegree() + 1]; + BigInteger x; + Set set = new HashSet(); + for (int i = 0; i < points.length; i++){ + x = new BigInteger(bits,random).mod(p); + if(set.contains(x)){ + i--; + continue; + } + set.add(x); + points[i] = new Polynomial.Point(x,polynomial); + } + return points; + } + + public void oneTest(Polynomial p, Polynomial.Point[] points) throws Exception { + Polynomial interpolation = Polynomial.interpolation(points,arithmetic); + assert (p.compareTo(interpolation) == 0); + } + + @Test + public void interpolationTest() throws Exception { + for (int i = 0; i < polynomials.length; i ++){ + oneTest(polynomials[i],pointsArrays[i]); + } + } +} diff --git a/distributed-key-generation/src/test/java/meerkat/crypto/secretsharing/shamir/PolynomialTests/MulByConstTest.java b/distributed-key-generation/src/test/java/meerkat/crypto/secretsharing/shamir/PolynomialTests/MulByConstTest.java new file mode 100644 index 0000000..77afc32 --- /dev/null +++ b/distributed-key-generation/src/test/java/meerkat/crypto/secretsharing/shamir/PolynomialTests/MulByConstTest.java @@ -0,0 +1,48 @@ +package meerkat.crypto.secretsharing.shamir.PolynomialTests; + +import meerkat.crypto.utils.GenerateRandomPolynomial; +import meerkat.crypto.utils.Z; +import meerkat.crypto.secretsharing.shamir.Polynomial; +import org.junit.Before; +import org.junit.Test; + +import java.math.BigInteger; +import java.util.Random; + +/** + * Created by Tzlil on 1/27/2016. + */ +public class MulByConstTest { + + + Polynomial[] arr1; + BigInteger[] arr2; + int tests = 1 << 12; + int maxDegree = 15; + int bits = 128; + Random random; + + @Before + public void settings(){ + random = new Random(); + arr1 = new Polynomial[tests]; + arr2 = new BigInteger[tests]; + for (int i = 0; i < arr1.length; i++){ + arr1[i] = GenerateRandomPolynomial.generateRandomPolynomial(random.nextInt(maxDegree),bits,random,new Z()); + arr2[i] = new BigInteger(bits,random); + } + } + + public void oneTest(Polynomial p, BigInteger c){ + Polynomial product = p.mul(c); + BigInteger x = new BigInteger(bits,random); + assert(product.evaluate(x).equals(p.evaluate(x).multiply(c))); + } + + @Test + public void mulByConstTest(){ + for (int i = 0 ; i < arr1.length; i ++){ + oneTest(arr1[i],arr2[i]); + } + } +} diff --git a/distributed-key-generation/src/test/java/meerkat/crypto/secretsharing/shamir/PolynomialTests/MulTest.java b/distributed-key-generation/src/test/java/meerkat/crypto/secretsharing/shamir/PolynomialTests/MulTest.java new file mode 100644 index 0000000..1f54247 --- /dev/null +++ b/distributed-key-generation/src/test/java/meerkat/crypto/secretsharing/shamir/PolynomialTests/MulTest.java @@ -0,0 +1,48 @@ +package meerkat.crypto.secretsharing.shamir.PolynomialTests; + +import meerkat.crypto.utils.GenerateRandomPolynomial; +import meerkat.crypto.utils.Z; +import meerkat.crypto.secretsharing.shamir.Polynomial; +import org.junit.Before; +import org.junit.Test; + +import java.math.BigInteger; +import java.util.Random; + +/** + * Created by Tzlil on 1/27/2016. + */ +public class MulTest { + + + Polynomial[] arr1; + Polynomial[] arr2; + int tests = 1 << 12; + int maxDegree = 15; + int bits = 128; + Random random; + + @Before + public void settings(){ + random = new Random(); + arr1 = new Polynomial[tests]; + arr2 = new Polynomial[tests]; + for (int i = 0; i < arr1.length; i++){ + arr1[i] = GenerateRandomPolynomial.generateRandomPolynomial(random.nextInt(maxDegree),bits,random,new Z()); + arr2[i] = GenerateRandomPolynomial.generateRandomPolynomial(random.nextInt(maxDegree),bits,random,new Z()); + } + } + + public void oneTest(Polynomial p1, Polynomial p2){ + Polynomial product = p1.mul(p2); + BigInteger x = new BigInteger(bits,random); + assert(product.evaluate(x).equals(p1.evaluate(x).multiply(p2.evaluate(x)))); + } + + @Test + public void mulTest(){ + for (int i = 0 ; i < arr1.length; i ++){ + oneTest(arr1[i],arr2[i]); + } + } +} diff --git a/distributed-key-generation/src/test/java/meerkat/crypto/secretsharing/shamir/SecretSharingTest.java b/distributed-key-generation/src/test/java/meerkat/crypto/secretsharing/shamir/SecretSharingTest.java new file mode 100644 index 0000000..021b529 --- /dev/null +++ b/distributed-key-generation/src/test/java/meerkat/crypto/secretsharing/shamir/SecretSharingTest.java @@ -0,0 +1,64 @@ +package meerkat.crypto.secretsharing.shamir; + +import meerkat.crypto.utils.concrete.Fp; +import meerkat.crypto.utils.GenerateRandomPrime; +import org.factcenter.qilin.primitives.CyclicGroup; +import org.factcenter.qilin.primitives.concrete.Zn; +import org.junit.Before; +import org.junit.Test; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Created by Tzlil on 1/29/2016. + */ +public class SecretSharingTest { + + SecretSharing[] secretSharingArray; + BigInteger[] secrets; + CyclicGroup group; + int tests = 1 << 10; + Random random; + BigInteger p = GenerateRandomPrime.SafePrime100Bits; + BigInteger q = p.subtract(BigInteger.ONE).divide(BigInteger.valueOf(2)); + + @Before + public void settings(){ + group = new Zn(q); + int t = 9; + int n = 20; + random = new Random(); + secretSharingArray = new SecretSharing[tests]; + secrets = new BigInteger[tests]; + + for (int i = 0; i < secretSharingArray.length; i++){ + secrets[i] = group.sample(random); + secretSharingArray[i] = new SecretSharing(t,n,secrets[i],random,q); + } + } + + public void oneTest(SecretSharing secretSharing, BigInteger secret) throws Exception { + int t = secretSharing.getT(); + int n = secretSharing.getN(); + Polynomial.Point[] shares = new Polynomial.Point[t + 1]; + List indexes = new ArrayList(n); + for (int i = 1 ; i <= n; i ++){ + indexes.add(i); + } + for (int i = 0 ; i < shares.length ; i++){ + shares[i] = secretSharing.getShare(indexes.remove(random.nextInt(indexes.size()))); + } + BigInteger calculated = SecretSharing.recoverSecret(shares,new Fp(q)); + assert (secret.equals(calculated)); + } + + @Test + public void secretSharingTest() throws Exception { + for (int i = 0 ; i < secretSharingArray.length; i ++){ + oneTest(secretSharingArray[i],secrets[i]); + } + } +} diff --git a/distributed-key-generation/src/test/java/meerkat/crypto/utils/BigIntegerByteEncoder.java b/distributed-key-generation/src/test/java/meerkat/crypto/utils/BigIntegerByteEncoder.java new file mode 100644 index 0000000..2781abb --- /dev/null +++ b/distributed-key-generation/src/test/java/meerkat/crypto/utils/BigIntegerByteEncoder.java @@ -0,0 +1,28 @@ +package meerkat.crypto.utils; + +import java.math.BigInteger; + +/** + * Created by Tzlil on 4/7/2016. + */ +public class BigIntegerByteEncoder implements org.factcenter.qilin.util.ByteEncoder { + @Override + public byte[] encode(BigInteger input) { + return input.toByteArray(); + } + + @Override + public BigInteger decode(byte[] input) { + return new BigInteger(1,input); + } + + @Override + public int getMinLength() { + return 0; + } + + @Override + public BigInteger denseDecode(byte[] input) { + return decode(input); + } +} diff --git a/distributed-key-generation/src/test/java/meerkat/crypto/utils/GenerateRandomPolynomial.java b/distributed-key-generation/src/test/java/meerkat/crypto/utils/GenerateRandomPolynomial.java new file mode 100644 index 0000000..802c479 --- /dev/null +++ b/distributed-key-generation/src/test/java/meerkat/crypto/utils/GenerateRandomPolynomial.java @@ -0,0 +1,30 @@ +package meerkat.crypto.utils; + +import meerkat.crypto.secretsharing.shamir.Polynomial; +import meerkat.crypto.utils.concrete.Fp; + +import java.math.BigInteger; +import java.util.Random; + +/** + * Created by Tzlil on 1/27/2016. + */ +public class GenerateRandomPolynomial { + + public static Polynomial generateRandomPolynomial(int degree, int bits, Random random, Arithmetic arithmetic) { + BigInteger[] coefficients = new BigInteger[degree + 1]; + + for (int i = 0 ; i <= degree; i++ ){ + coefficients[i] = new BigInteger(bits,random); // sample from Zp [0,... q-1] + } + return new Polynomial(coefficients,arithmetic); + } + + public static Polynomial generateRandomPolynomial(int degree,int bits,Random random,BigInteger p) { + BigInteger[] coefficients = generateRandomPolynomial(degree,bits,random,new Fp(p)).getCoefficients(); + for (int i = 0; i { + @Override + public BigInteger add(BigInteger a, BigInteger b) { + return a.add(b); + } + + @Override + public BigInteger sub(BigInteger a, BigInteger b) { + return a.subtract(b); + } + + @Override + public BigInteger mul(BigInteger a, BigInteger b) { + return a.multiply(b); + } + + @Override + public BigInteger div(BigInteger a, BigInteger b) { + return a.divide(b); + } +} diff --git a/meerkat-common/build.gradle b/meerkat-common/build.gradle index b44b1fe..6bf5ad7 100644 --- a/meerkat-common/build.gradle +++ b/meerkat-common/build.gradle @@ -39,6 +39,7 @@ version += "${isSnapshot ? '-SNAPSHOT' : ''}" dependencies { // Logging compile 'org.slf4j:slf4j-api:1.7.7' + compile 'javax.ws.rs:javax.ws.rs-api:2.0.+' runtime 'ch.qos.logback:logback-classic:1.1.2' runtime 'ch.qos.logback:logback-core:1.1.2' @@ -46,7 +47,7 @@ dependencies { compile 'com.google.protobuf:protobuf-java:3.+' // ListeningExecutor - compile 'com.google.guava:guava:11.0.+' + compile 'com.google.guava:guava:15.0' // Crypto compile 'org.factcenter.qilin:qilin:1.2.+' diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/AsyncBulletinBoardClient.java b/meerkat-common/src/main/java/meerkat/bulletinboard/AsyncBulletinBoardClient.java new file mode 100644 index 0000000..ab71a76 --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/AsyncBulletinBoardClient.java @@ -0,0 +1,122 @@ +package meerkat.bulletinboard; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.protobuf.Timestamp; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.protobuf.Crypto.Signature; + +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 14-Dec-15. + */ +public interface AsyncBulletinBoardClient extends BulletinBoardClient { + + /** + * Post a message to the bulletin board in an asynchronous manner + * The message may be broken up by the client into a batch message, depending on implementation + * @param msg is the message to be posted + * @param callback is a class containing methods to handle the result of the operation + * @return a unique message ID for the message, that can be later used to retrieve the batch + */ + public MessageID postMessage(BulletinBoardMessage msg, FutureCallback callback); + + /** + * Perform an end-to-end post of a message in batch form + * @param completeBatch contains all the data of the batch + * @param chunkSize is the maximum size of each chunk of the message in bytes + * @param callback is a class containing methods to handle the result of the operation + * @return a unique identifier for the batch message + */ + public MessageID postAsBatch(BulletinBoardMessage completeBatch, int chunkSize, FutureCallback callback); + + /** + * An interface for returning an opaque identifier for a batch message + * This identifier is used to uniquely identify the batch until it is completely posted and signed + * After the batch is fully posted: it is identified by its digest (like any message) + * This can be implementation-specific (and not necessarily interchangeable between different implementations) + */ + public interface BatchIdentifier {} + + /** + * This message informs the server about the existence of a new batch message and supplies it with the tags associated with it + * @param tags contains the tags used in the batch + * @param callback is a callback function class for handling results of the operation + * it receives a BatchIdentifier for use in subsequent batch post operations + */ + public void beginBatch(Iterable tags, FutureCallback callback); + + /** + * This method posts batch data into an (assumed to be open) batch + * It does not close the batch + * @param batchIdentifier is the temporary batch identifier + * @param batchChunkList is the (canonically ordered) list of data comprising the portion of the batch to be posted + * @param startPosition is the location (in the batch) of the first entry in batchDataList + * (optionally used to continue interrupted post operations) + * The first position in the batch is position 0 + * @param callback is a callback function class for handling results of the operation + * @throws IllegalArgumentException if the batch identifier given was of an illegal format + */ + public void postBatchData(BatchIdentifier batchIdentifier, List batchChunkList, + int startPosition, FutureCallback callback) throws IllegalArgumentException; + + /** + * Overloading of the postBatchData method which starts at the first position in the batch + */ + public void postBatchData(BatchIdentifier batchIdentifier, List batchChunkList, FutureCallback callback) + throws IllegalArgumentException; + + /** + * Attempts to close a batch message + * @param batchIdentifier is the temporary batch identifier + * @param callback is a callback function class for handling results of the operation + * @throws IllegalArgumentException if the batch identifier given was of an illegal format + */ + public void closeBatch(BatchIdentifier batchIdentifier, Timestamp timestamp, Iterable signatures, FutureCallback callback) + throws IllegalArgumentException; + + /** + * Check how "safe" a given message is in an asynchronous manner + * The result of the computation is a rank between 0.0 and 1.0 indicating the fraction of servers containing the message + * @param id is the unique message identifier for retrieval + * @param callback is a callback function class for handling results of the operation + */ + public void getRedundancy(MessageID id, FutureCallback callback); + + /** + * Read all messages posted matching the given filter in an asynchronous manner + * Note that if messages haven't been "fully posted", this might return a different + * set of messages in different calls. However, messages that are fully posted + * are guaranteed to be included + * Also: batch messages are returned as stubs. + * @param filterList return only messages that match the filters (null means no filtering) + * @param callback is a callback function class for handling results of the operation + */ + public void readMessages(MessageFilterList filterList, FutureCallback> callback); + + /** + * Read a given message from the bulletin board + * If the message is a batch: returns a complete message containing the batch data as well as the metadata + * @param msgID is the ID of the message to be read + * @param callback is a callback class for handling the result of the operation + */ + public void readMessage(MessageID msgID, FutureCallback callback); + + /** + * Read batch data for a specific stub message + * @param stub is a batch message stub + * @param callback is a callback class for handling the result of the operation + * @return a new BulletinBoardMessage containing both metadata from the stub and actual data from the server + * @throws IllegalArgumentException if the received message is not a stub + */ + public void readBatchData(BulletinBoardMessage stub, FutureCallback callback) throws IllegalArgumentException; + + + /** + * Perform a Sync Query on the bulletin board + * @param syncQuery defines the query + * @param callback is a callback for handling the result of the query + */ + public void querySync(SyncQuery syncQuery, FutureCallback callback); + +} diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardClient.java b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardClient.java index c51e561..c03051c 100644 --- a/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardClient.java +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardClient.java @@ -1,6 +1,6 @@ package meerkat.bulletinboard; -import meerkat.comm.*; +import meerkat.comm.CommunicationException; import meerkat.protobuf.Voting.*; import static meerkat.protobuf.BulletinBoardAPI.*; @@ -12,11 +12,6 @@ import java.util.List; */ public interface BulletinBoardClient { - interface ClientCallback { - void handleCallback(T msg); - void handleFailure(Throwable t); - } - /** * Initialize the client to use some specified servers * @param clientParams contains the parameters required for the client setup @@ -24,26 +19,69 @@ public interface BulletinBoardClient { void init(BulletinBoardClientParams clientParams); /** - * Post a message to the bulletin board - * @param msg + * Post a message to the bulletin board in a synchronous manner + * The message may be broken up by the client into a batch message depending on implementation + * @param msg is the message to be posted + * @return a unique message ID for the message, that can be later used to retrieve the batch + * @throws CommunicationException */ - MessageID postMessage(BulletinBoardMessage msg, ClientCallback callback); + MessageID postMessage(BulletinBoardMessage msg) throws CommunicationException; /** - * Check how "safe" a given message is - * @param id + * Check how "safe" a given message is in a synchronous manner + * @param id is the unique message identifier for retrieval * @return a normalized "redundancy score" from 0 (local only) to 1 (fully published) + * @throws CommunicationException */ - void getRedundancy(MessageID id, ClientCallback callback); + float getRedundancy(MessageID id) throws CommunicationException; /** - * Read all messages posted matching the given filter + * Read all messages posted matching the given filter in a synchronous manner * Note that if messages haven't been "fully posted", this might return a different * set of messages in different calls. However, messages that are fully posted * are guaranteed to be included. - * @param filterList return only messages that match the filters (null means no filtering). + * Also: batch messages are returned as stubs. + * @param filterList return only messages that match the filters (null means no filtering) + * @return the list of messages */ - void readMessages(MessageFilterList filterList, ClientCallback> callback); + List readMessages(MessageFilterList filterList) throws CommunicationException; + + /** + * Breaks up a bulletin board message into chunks and posts it as a batch message + * @param msg is the message to post + * @param chunkSize is the maximal chunk size in bytes + * @return the unique message ID + * @throws CommunicationException if operation is unsuccessful + */ + MessageID postAsBatch(BulletinBoardMessage msg, int chunkSize) throws CommunicationException; + + /** + * Read a given message from the bulletin board + * If the message is a batch: returns a complete message containing the batch data as well as the metadata + * @param msgID is the ID of the message to be read + * @return the complete message + * @throws CommunicationException if operation is unsuccessful + */ + BulletinBoardMessage readMessage(MessageID msgID) throws CommunicationException; + + /** + * Read batch data for a specific stub message + * @param stub is a batch message stub + * @return a new BulletinBoardMessage containing both metadata from the stub and actual data from the server + * @throws CommunicationException if operation is unsuccessful + * @throws IllegalArgumentException if the received message is not a stub + */ + BulletinBoardMessage readBatchData(BulletinBoardMessage stub) throws CommunicationException, IllegalArgumentException; + + /** + * Create a SyncQuery to test against that corresponds with the current server state for a specific filter list + * Should only be called on instances for which the actual server contacted is known (i.e. there is only one server) + * @param generateSyncQueryParams defines the required information needed to generate the query + * These are represented as fractions of the total number of relevant messages + * @return The generated SyncQuery + * @throws CommunicationException when no DB can be contacted + */ + SyncQuery generateSyncQuery(GenerateSyncQueryParams generateSyncQueryParams) throws CommunicationException; /** * Closes all connections, if any. diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardConstants.java b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardConstants.java new file mode 100644 index 0000000..9404165 --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardConstants.java @@ -0,0 +1,21 @@ +package meerkat.bulletinboard; + +/** + * Created by Arbel Deutsch Peled on 21-Dec-15. + */ +public interface BulletinBoardConstants { + + // Relative addresses for Bulletin Board operations + + public static final String BULLETIN_BOARD_SERVER_PATH = "/bbserver"; + public static final String GENERATE_SYNC_QUERY_PATH = "/generatesyncquery"; + public static final String READ_MESSAGES_PATH = "/readmessages"; + public static final String COUNT_MESSAGES_PATH = "/countmessages"; + public static final String READ_BATCH_PATH = "/readbatch"; + public static final String POST_MESSAGE_PATH = "/postmessage"; + public static final String BEGIN_BATCH_PATH = "/beginbatch"; + public static final String POST_BATCH_PATH = "/postbatch"; + public static final String CLOSE_BATCH_PATH = "/closebatch"; + public static final String SYNC_QUERY_PATH = "/syncquery"; + +} diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardDigest.java b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardDigest.java new file mode 100644 index 0000000..3e47c23 --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardDigest.java @@ -0,0 +1,30 @@ +package meerkat.bulletinboard; + +import meerkat.crypto.Digest; +import meerkat.protobuf.BulletinBoardAPI.*; + +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 18-Dec-15. + * Extends the Digest interface with methods for digesting Bulletin Board messages + */ +public interface BulletinBoardDigest extends Digest { + + /** + * Update the digest with the message data (ignore the signature) + * The digest only uses the part the signatures are computed on for this operation + * If the message is a stub: this should be called before digesting the raw data + * @param msg is the message that needs to be digested + */ + public void update(BulletinBoardMessage msg); + + /** + * Update the digest with the message data (ignore the signature) + * The digest only uses the part the signatures are computed on for this operation + * If the message is a stub: this should be called before digesting the raw data + * @param msg is the message that needs to be digested + */ + public void update(UnsignedBulletinBoardMessage msg); + +} diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardMessageDeleter.java b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardMessageDeleter.java new file mode 100644 index 0000000..cf57975 --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardMessageDeleter.java @@ -0,0 +1,49 @@ +package meerkat.bulletinboard; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.comm.CommunicationException; +import meerkat.protobuf.BulletinBoardAPI.*; + +/** + * Created by Arbel Deutsch Peled on 13-Apr-16. + * This interface is meant to extend a BulletinBoardClient interface/class + * It provides it with the ability to delete messages from the Server + * This assumes the Server implements the {@link DeletableBulletinBoardServer} + */ +public interface BulletinBoardMessageDeleter { + + /** + * Deletes a message from a Bulletin Board Server in a possibly asynchronous manner + * Logs this action + * @param msgID is the ID of the message to delete + * @param callback handles the result of the operation + */ + public void deleteMessage(MessageID msgID, FutureCallback callback); + + /** + * Deletes a message from the Bulletin Board in a possibly asynchronous manner + * Logs this action + * @param entryNum is the serial entry number of the message to delete + * @param callback handles the result of the operation + */ + public void deleteMessage(long entryNum, FutureCallback callback); + + /** + * Deletes a message from a Bulletin Board Server in a synchronous manner + * Logs this action + * @param msgID is the ID of the message to delete + * @return TRUE if the message was deleted and FALSE if it did not exist on the server + * @throws CommunicationException when an error occurs + */ + public boolean deleteMessage(MessageID msgID) throws CommunicationException; + + /** + * Deletes a message from the Bulletin Board in a synchronous manner + * Logs this action + * @param entryNum is the serial entry number of the message to delete + * @return TRUE if the message was deleted and FALSE if it did not exist on the server + * @throws CommunicationException when an error occurs + */ + public boolean deleteMessage(long entryNum) throws CommunicationException; + +} diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardServer.java b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardServer.java index da53c1f..40f8ab3 100644 --- a/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardServer.java +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardServer.java @@ -1,42 +1,110 @@ package meerkat.bulletinboard; +import com.google.protobuf.BoolValue; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; import meerkat.comm.CommunicationException; +import meerkat.comm.MessageOutputStream; import meerkat.protobuf.BulletinBoardAPI.*; + /** * Created by Arbel on 07/11/15. * - * This interface refers to a single instance of a Bulletin Board. - * An implementation of this interface may use any DB and be hosted on any machine. + * This interface refers to a single instance of a Bulletin Board + * An implementation of this interface may use any DB and be hosted on any machine. */ public interface BulletinBoardServer{ /** - * This method initializes the server by reading the signature data and storing it. - * It also establishes the connection to the DB. - * @throws CommunicationException on DB connection error. + * This method initializes the server by reading the signature data and storing it + * It also establishes the connection to the DB + * @throws CommunicationException on DB connection error */ - public void init(String meerkatDB) throws CommunicationException; + public void init() throws CommunicationException; /** * Post a message to bulletin board. * @param msg is the actual (signed) message - * @return TRUE if the message has been authenticated and FALSE otherwise (in ProtoBuf form). - * @throws CommunicationException on DB connection error. + * @return TRUE if the message has been authenticated and FALSE otherwise (in ProtoBuf form) + * @throws CommunicationException on DB connection error */ - public BoolMsg postMessage(BulletinBoardMessage msg) throws CommunicationException; - + public BoolValue postMessage(BulletinBoardMessage msg) throws CommunicationException; + /** - * Read all messages posted matching the given filter. - * @param filter return only messages that match the filter (empty list means no filtering). - * @return + * Read all posted messages matching the given filters + * @param filterList return only messages that match the filters (empty list or null means no filtering) + * @param out is an output stream into which the matching messages are written + * @throws CommunicationException on DB connection error */ - BulletinBoardMessageList readMessages(MessageFilterList filterList) throws CommunicationException; - - /** - * This method closes the connection to the DB. - * @throws CommunicationException on DB connection error. + public void readMessages(MessageFilterList filterList, MessageOutputStream out) throws CommunicationException; + + /** + * Return the number of posted messages matching the given filters + * @param filterList count only messages that match the filters (empty list or null means no filtering) + * @return an IntMsg containing the number of messages that match the filter + * @throws CommunicationException on DB connection error */ - public void close() throws CommunicationException; + public Int32Value getMessageCount(MessageFilterList filterList) throws CommunicationException; + + /** + * Informs server about a new batch message + * @param message contains the required data about the new batch + * @return a unique batch identifier for the new batch ; -1 if batch creation was unsuccessful + * @throws CommunicationException on DB connection error + */ + public Int64Value beginBatch(BeginBatchMessage message) throws CommunicationException; + + /** + * Posts a chunk of a batch message to the bulletin board + * Note that the existence and contents of a batch message are not available for reading before the batch is finalized + * @param batchMessage contains the (partial) data this message carries as well as meta-data required in order to place the data + * in the correct position inside the correct batch + * @return TRUE if the message is accepted and successfully saved and FALSE otherwise + * Specifically, if the batch is already closed: the value returned will be FALSE + * However, requiring to open a batch before insertion of messages is implementation-dependent + * @throws CommunicationException on DB connection error + */ + public BoolValue postBatchMessage(BatchMessage batchMessage) throws CommunicationException; + + /** + * Attempts to close and finalize a batch message + * @param message contains the data necessary to close the batch; in particular: the signature for the batch + * @return TRUE if the batch was successfully closed, FALSE otherwise + * Specifically, if the signature is invalid or if some of the batch parts have not yet been submitted: the value returned will be FALSE + * @throws CommunicationException on DB connection error + */ + public BoolValue closeBatch(CloseBatchMessage message) throws CommunicationException; + + /** + * Reads a batch message from the server (starting with the supplied position) + * @param batchQuery specifies which batch and what parts of it to retrieve + * @param out is a stream of the ordered batch messages starting from the specified start position (if given) or from the beginning (if omitted) + * @throws CommunicationException on DB connection error + * @throws IllegalArgumentException if message ID does not specify a batch + */ + public void readBatch(BatchQuery batchQuery, MessageOutputStream out) throws CommunicationException, IllegalArgumentException; + + /** + * Create a SyncQuery to test against that corresponds with the current server state for a specific filter list + * @param generateSyncQueryParams defines the information needed to generate the query + * @return The generated SyncQuery + * @throws CommunicationException on DB connection error + */ + public SyncQuery generateSyncQuery(GenerateSyncQueryParams generateSyncQueryParams) throws CommunicationException; + + /** + * Queries the database for sync status with respect to a given sync query + * @param syncQuery contains a succinct representation of states to compare to + * @return a SyncQueryResponse object containing the representation of the most recent state the database matches + * @throws CommunicationException + */ + public SyncQueryResponse querySync(SyncQuery syncQuery) throws CommunicationException; + + /** + * This method closes the connection to the DB + * @throws CommunicationException on DB connection error + */ + public void close() throws CommunicationException; } diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardSignature.java b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardSignature.java new file mode 100644 index 0000000..ba018ca --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardSignature.java @@ -0,0 +1,31 @@ +package meerkat.bulletinboard; + +import meerkat.crypto.DigitalSignature; +import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage; +import meerkat.protobuf.BulletinBoardAPI.UnsignedBulletinBoardMessage; +import meerkat.protobuf.Crypto; + +import java.security.SignatureException; + +/** + * Created by Arbel Deutsch Peled on 18-Dec-15. + * Extends the DigitalSignature interface with methods for signing Bulletin Board messages + */ +public interface BulletinBoardSignature extends DigitalSignature { + + /** + * Add msg to the content stream to be verified / signed + * The digest only uses the part the signatures are computed on for this operation + * If the message is a stub: this should be called before updating with the raw data + * @param msg is the message that needs to be digested + */ + public void updateContent(BulletinBoardMessage msg) throws SignatureException; + + /** + * Add msg to the content stream to be verified / signed + * If the message is a stub: this should be called before updating with the raw data + * @param msg is the message that needs to be digested + */ + public void updateContent(UnsignedBulletinBoardMessage msg) throws SignatureException; + +} diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardSubscriber.java b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardSubscriber.java new file mode 100644 index 0000000..85eb2cc --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardSubscriber.java @@ -0,0 +1,32 @@ +package meerkat.bulletinboard; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage; +import meerkat.protobuf.BulletinBoardAPI.MessageFilterList; + +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 03-Mar-16. + * This interface defines the behaviour required from a subscription service to Bulletin Board messages + */ +public interface BulletinBoardSubscriber { + + /** + * Subscribes to a notifier that will return any new messages on the server that match the given filters + * In case of communication error: the subscription is terminated + * @param filterList defines the set of filters for message retrieval + * @param callback defines how to handle new messages received and/or a failures in communication + */ + public void subscribe(MessageFilterList filterList, FutureCallback> callback); + + /** + * Subscribes to a notifier that will return any new messages on the server that match the given filters + * In case of communication error: the subscription is terminated + * @param filterList defines the set of filters for message retrieval + * @param startEntry defines the first entry number to consider + * @param callback defines how to handle new messages received and/or a failures in communication + */ + public void subscribe(MessageFilterList filterList, long startEntry, FutureCallback> callback); + +} diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardSynchronizer.java b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardSynchronizer.java new file mode 100644 index 0000000..c25d737 --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/BulletinBoardSynchronizer.java @@ -0,0 +1,84 @@ +package meerkat.bulletinboard; + +import meerkat.comm.CommunicationException; +import meerkat.protobuf.BulletinBoardAPI.*; + +import com.google.common.util.concurrent.FutureCallback; + +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 08-Mar-16. + * This interface defines the behaviour of a bulletin board synchronizer + * This is used to make sure that data in a specific instance of a bulletin board server is duplicated to a sufficient percentage of the other servers + */ +public interface BulletinBoardSynchronizer extends Runnable { + + public enum SyncStatus{ + SYNCHRONIZED, // No more messages to upload + PENDING, // Synchronizer is querying for data to upload and uploading it as needed + SERVER_ERROR, // Synchronizer encountered an error while uploading, but will retry + STOPPED // Stopped/Not started by user + } + + /** + * Initializes the synchronizer with the required data to function properly + * @param localClient is a client for the temporary local storage server which contains only data to be uploaded + * @param remoteClient is a client for the remote servers into which the data needs to be uploaded + */ + public void init(DeletableSubscriptionBulletinBoardClient localClient, AsyncBulletinBoardClient remoteClient); + + /** + * Returns the current server synchronization status + * @return the current synchronization status + */ + public SyncStatus getSyncStatus(); + + /** + * Creates a subscription to sync status changes + * @param callback is the handler for any status changes + */ + public void subscribeToSyncStatus(FutureCallback callback); + + /** + * Returns the messages which have not yet been synchronized + * @return the list of messages remaining to be synchronized + */ + public List getRemainingMessages() throws CommunicationException; + + /** + * Asynchronously returns the messages which have not yet been synchronized + * @param callback is the handler for the list of messages + */ + public void getRemainingMessages(FutureCallback> callback); + + /** + * Returns the current number of unsynchronized messages + * @return the current synchronization status + */ + public long getRemainingMessagesCount() throws CommunicationException; + + /** + * Creates a subscription to changes in the number of unsynchronized messages + * @param callback is the handler for any status changes + */ + public void subscribeToRemainingMessagesCount(FutureCallback callback); + + /** + * Starts the synchronization + */ + @Override + public void run(); + + /** + * Lets the Synchronizer know that there is new data to be uploaded + * This is used to reduce the latency between local data-writes and uploads to the remote servers + */ + public void nudge(); + + /** + * Stops the synchronization + */ + public void stop(); + +} diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/Checksum.java b/meerkat-common/src/main/java/meerkat/bulletinboard/Checksum.java new file mode 100644 index 0000000..b8ddb0b --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/Checksum.java @@ -0,0 +1,122 @@ +package meerkat.bulletinboard; + +import com.google.protobuf.ByteString; +import meerkat.crypto.Digest; +import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage; +import meerkat.protobuf.BulletinBoardAPI.MessageID; + +import java.util.Collection; + +/** + * Created by Arbel Deutsch Peled on 01-Mar-16. + * This interface is used to create checksums of Bulletin Board messages IDs + * This is useful in comparing database states + */ +public interface Checksum { + + /** + * Sets the Digest method which is used in creating message IDs from the messages themselves + * This method must be called with an initialized Digest before calling any methods that receive a parameter of type BulletinBoardMessage + * @param digest is the Digest that will be used to create message IDs from Bulletin Board Messages + */ + public void setDigest(Digest digest); + + /** + * Used to reset the current checksum state + */ + public void reset(); + + /** + * Update the current checksum with the given ID + * @param messageID is the message ID to be added + */ + public void update(MessageID messageID); + + /** + * Update the current checksum with the given collection of IDs + * @param messageIDs contains the message IDs + */ + public void update(Collection messageIDs); + + /** + * Update the current checksum with the given ID + * @param messageID is the message ID to be added + */ + public void update(ByteString messageID); + + /** + * Update the current checksum with the given ID + * @param messageID is the message ID to be added + */ + public void update(byte[] messageID); + + /** + * Update the current checksum with the message ID of the given message + * @param bulletinBoardMessage is the message whose ID should be added to the checksum + * @throws IllegalStateException if a Digest has not been set before calling this method + */ + public void digestAndUpdate(BulletinBoardMessage bulletinBoardMessage) throws IllegalStateException; + + + /** + * Update the current checksum with the message IDs of the given messages + * @param bulletinBoardMessages contains the messages whose IDs should be added to the checksum + * @throws IllegalStateException if a Digest has not been set before calling this method + */ + public void digestAndUpdate(Collection bulletinBoardMessages) throws IllegalStateException; + + /** + * Returns the current checksum without changing the checksum state + * @return the current checksum + */ + public long getChecksum(); + + /** + * Updates the current checksum with the given ID and returns the resulting checksum + * The checksum is not reset afterwards + * @param messageID is the message ID to be added + * @return the updated checksum + */ + public long getChecksum(MessageID messageID); + + /** + * Updates the current checksum with the given ID and returns the resulting checksum + * The checksum is not reset afterwards + * @param messageID is the message ID to be added + * @return the updated checksum + */ + public long getChecksum(ByteString messageID); + + /** + * Updates the current checksum with the given ID and returns the resulting checksum + * The checksum is not reset afterwards + * @param messageID is the message ID to be added + * @return the updated checksum + */ + public long getChecksum(byte[] messageID); + + /** + * Updates the current checksum with the given IDs and returns the resulting checksum + * The checksum is not reset afterwards + * @param messageIDs contains the message IDs to be added + * @return the updated checksum + */ + public long getChecksum(Collection messageIDs); + + /** + * Updates the current checksum with the message ID of the given message + * The checksum is not reset afterwards + * @param bulletinBoardMessage is the message whose ID should be added to the checksum + * @return the updated checksum + */ + public long digestAndGetChecksum(BulletinBoardMessage bulletinBoardMessage) throws IllegalStateException; + + /** + * Updates the current checksum with the message IDs of the given messages + * The checksum is not reset afterwards + * @param bulletinBoardMessages contains the messages whose IDs should be added to the checksum + * @return the updated checksum + */ + public long digestAndGetChecksum(Collection bulletinBoardMessages) throws IllegalStateException; + +} diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/DeletableBulletinBoardServer.java b/meerkat-common/src/main/java/meerkat/bulletinboard/DeletableBulletinBoardServer.java new file mode 100644 index 0000000..cfff084 --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/DeletableBulletinBoardServer.java @@ -0,0 +1,32 @@ +package meerkat.bulletinboard; + +import com.google.protobuf.BoolValue; +import meerkat.comm.CommunicationException; +import meerkat.protobuf.BulletinBoardAPI.*; + +/** + * Created by Arbel Deutsch Peled on 13-Apr-16. + */ +public interface DeletableBulletinBoardServer extends BulletinBoardServer { + + /** + * Deletes a message from the Bulletin Board + * If the message is a batch: the batch data is deleted as well + * Logs this action + * @param msgID is the ID of the message to delete + * @return a BoolMsg containing the value TRUE if a message was deleted, FALSE if the message does not exist + * @throws CommunicationException in case of an error + */ + public BoolValue deleteMessage(MessageID msgID) throws CommunicationException; + + /** + * Deletes a message from the Bulletin Board + * If the message is a batch: the batch data is deleted as well + * Logs this action + * @param entryNum is the serial entry number of the message to delete + * @return a BoolMsg containing the value TRUE if a message was deleted, FALSE if the message does not exist + * @throws CommunicationException in case of an error + */ + public BoolValue deleteMessage(long entryNum) throws CommunicationException; + +} diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/DeletableSubscriptionBulletinBoardClient.java b/meerkat-common/src/main/java/meerkat/bulletinboard/DeletableSubscriptionBulletinBoardClient.java new file mode 100644 index 0000000..acc29fe --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/DeletableSubscriptionBulletinBoardClient.java @@ -0,0 +1,7 @@ +package meerkat.bulletinboard; + +/** + * Created by Arbel Deutsch Peled on 13-Apr-16. + */ +public interface DeletableSubscriptionBulletinBoardClient extends SubscriptionBulletinBoardClient, BulletinBoardMessageDeleter { +} diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/GenericBulletinBoardDigest.java b/meerkat-common/src/main/java/meerkat/bulletinboard/GenericBulletinBoardDigest.java new file mode 100644 index 0000000..fb7bd3f --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/GenericBulletinBoardDigest.java @@ -0,0 +1,74 @@ +package meerkat.bulletinboard; + +import com.google.protobuf.ByteString; +import com.google.protobuf.BytesValue; +import com.google.protobuf.Message; +import meerkat.crypto.Digest; +import meerkat.protobuf.BulletinBoardAPI; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.protobuf.BulletinBoardAPI.MessageID; + + +/** + * Created by Arbel Deutsch Peled on 19-Dec-15. + * Wrapper class for digesting Batches in a standardized way + */ +public class GenericBulletinBoardDigest implements BulletinBoardDigest { + + private Digest digest; + + public GenericBulletinBoardDigest(Digest digest) { + this.digest = digest; + } + + @Override + public byte[] digest() { + return digest.digest(); + } + + @Override + public MessageID digestAsMessageID() { + return digest.digestAsMessageID(); + } + + @Override + public void update(Message msg) { + digest.update(msg); + } + + @Override + public void update(byte[] data) { + digest.update(data); + } + + @Override + public void reset() { + digest.reset(); + } + + @Override + public GenericBulletinBoardDigest clone() throws CloneNotSupportedException{ + return new GenericBulletinBoardDigest(digest.clone()); + } + + @Override + public void update(BulletinBoardMessage msg) { + update(msg.getMsg()); + } + + @Override + public void update(UnsignedBulletinBoardMessage msg) { + + for (ByteString tag : msg.getTagList().asByteStringList()){ + update(tag.toByteArray()); + } + + update(msg.getTimestamp()); + + if (msg.getDataTypeCase() == UnsignedBulletinBoardMessage.DataTypeCase.DATA){ + update(msg.getData().toByteArray()); + } + + } + +} diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/GenericBulletinBoardSignature.java b/meerkat-common/src/main/java/meerkat/bulletinboard/GenericBulletinBoardSignature.java new file mode 100644 index 0000000..aad6466 --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/GenericBulletinBoardSignature.java @@ -0,0 +1,106 @@ +package meerkat.bulletinboard; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Message; +import meerkat.crypto.Digest; +import meerkat.crypto.DigitalSignature; +import meerkat.protobuf.BulletinBoardAPI; +import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage; +import meerkat.protobuf.BulletinBoardAPI.MessageID; +import meerkat.protobuf.BulletinBoardAPI.UnsignedBulletinBoardMessage; +import meerkat.protobuf.Crypto; + +import java.io.IOException; +import java.io.InputStream; +import java.security.*; +import java.security.cert.CertificateException; + + +/** + * Created by Arbel Deutsch Peled on 19-Dec-15. + * Wrapper class for digesting Batches in a standardized way + */ +public class GenericBulletinBoardSignature implements BulletinBoardSignature { + + private DigitalSignature signer; + + public GenericBulletinBoardSignature(DigitalSignature signer) { + this.signer = signer; + } + + @Override + public void updateContent(BulletinBoardMessage msg) throws SignatureException{ + signer.updateContent(msg.getMsg()); + } + + @Override + public void updateContent(UnsignedBulletinBoardMessage msg) throws SignatureException{ + + for (ByteString tag : msg.getTagList().asByteStringList()){ + updateContent(tag.toByteArray()); + } + + updateContent(msg.getTimestamp()); + + if (msg.getDataTypeCase() == UnsignedBulletinBoardMessage.DataTypeCase.DATA){ + updateContent(msg.getData().toByteArray()); + } + + } + + @Override + public void loadVerificationCertificates(InputStream certStream) throws CertificateException { + signer.loadVerificationCertificates(certStream); + } + + @Override + public void clearVerificationCertificates() { + signer.clearVerificationCertificates(); + } + + @Override + public void updateContent(byte[] data) throws SignatureException { + signer.updateContent(data); + } + + @Override + public void updateContent(Message msg) throws SignatureException { + signer.updateContent(msg); + } + + @Override + public Crypto.Signature sign() throws SignatureException { + return signer.sign(); + } + + @Override + public void initVerify(Crypto.Signature sig) throws CertificateException, InvalidKeyException { + signer.initVerify(sig); + } + + @Override + public boolean verify() { + return signer.verify(); + } + + @Override + public KeyStore.Builder getPKCS12KeyStoreBuilder(InputStream keyStream, char[] password) throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException { + return signer.getPKCS12KeyStoreBuilder(keyStream, password); + } + + @Override + public void loadSigningCertificate(KeyStore.Builder keyStoreBuilder) throws IOException, CertificateException, UnrecoverableKeyException { + signer.loadSigningCertificate(keyStoreBuilder); + } + + @Override + public ByteString getSignerID() { + return signer.getSignerID(); + } + + @Override + public void clearSigningKey() { + signer.clearSigningKey(); + } + +} diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/SimpleChecksum.java b/meerkat-common/src/main/java/meerkat/bulletinboard/SimpleChecksum.java new file mode 100644 index 0000000..90d7ae5 --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/SimpleChecksum.java @@ -0,0 +1,131 @@ +package meerkat.bulletinboard; + +import com.google.protobuf.ByteString; +import meerkat.crypto.Digest; +import meerkat.protobuf.BulletinBoardAPI.MessageID; +import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage; + +import java.util.Collection; + +/** + * Created by Arbel Deutsch Peled on 01-Mar-16. + * Implementation of Checksum via bitwise XOR of the bytes of message IDs + */ +public class SimpleChecksum implements Checksum{ + + private Digest digest; + private long checksum; + + public SimpleChecksum() { + digest = null; + reset(); + } + + @Override + public void setDigest(Digest digest) { + this.digest = digest; + } + + @Override + public void reset() { + checksum = 0; + } + + @Override + public void update(MessageID messageID) { + ByteString messageIDByteString = messageID.getID(); + update(messageIDByteString); + } + + @Override + public void update(Collection messageIDs) { + + for (MessageID messageID : messageIDs){ + update(messageID); + } + + } + + @Override + public void update(ByteString messageID) { + for (int i = 0 ; i < messageID.size() ; i++){ + checksum &= messageID.byteAt(i); + } + } + + @Override + public void update(byte[] messageID) { + for (int i = 0 ; i < messageID.length ; i++){ + checksum &= messageID[i]; + } + } + + private void checkDigest() throws IllegalStateException { + + if (digest == null){ + throw new IllegalStateException("Digest method not set. Use setDigest method before calling digestAndUpdate."); + } + + } + + @Override + public void digestAndUpdate(BulletinBoardMessage bulletinBoardMessage) throws IllegalStateException { + + checkDigest(); + + digest.reset(); + digest.update(bulletinBoardMessage); + update(digest.digest()); + + } + + @Override + public void digestAndUpdate(Collection bulletinBoardMessages) throws IllegalStateException { + + for (BulletinBoardMessage bulletinBoardMessage : bulletinBoardMessages){ + digestAndUpdate(bulletinBoardMessage); + } + + } + + @Override + public long getChecksum() { + return checksum; + } + + @Override + public long getChecksum(MessageID messageID) { + update(messageID); + return getChecksum(); + } + + @Override + public long getChecksum(ByteString messageID) { + update(messageID); + return getChecksum(); + } + + @Override + public long getChecksum(byte[] messageID) { + update(messageID); + return getChecksum(); + } + + @Override + public long getChecksum(Collection messageIDs) { + update(messageIDs); + return getChecksum(); + } + + @Override + public long digestAndGetChecksum(BulletinBoardMessage bulletinBoardMessage) throws IllegalStateException { + digestAndUpdate(bulletinBoardMessage); + return getChecksum(); + } + + @Override + public long digestAndGetChecksum(Collection bulletinBoardMessages) throws IllegalStateException { + digestAndUpdate(bulletinBoardMessages); + return getChecksum(); + } +} diff --git a/meerkat-common/src/main/java/meerkat/bulletinboard/SubscriptionBulletinBoardClient.java b/meerkat-common/src/main/java/meerkat/bulletinboard/SubscriptionBulletinBoardClient.java new file mode 100644 index 0000000..5577bac --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/bulletinboard/SubscriptionBulletinBoardClient.java @@ -0,0 +1,7 @@ +package meerkat.bulletinboard; + +/** + * Created by Arbel Deutsch Peled on 03-Mar-16. + */ +public interface SubscriptionBulletinBoardClient extends AsyncBulletinBoardClient, BulletinBoardSubscriber { +} diff --git a/meerkat-common/src/main/java/meerkat/comm/Channel.java b/meerkat-common/src/main/java/meerkat/comm/Channel.java new file mode 100644 index 0000000..7dd74c1 --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/comm/Channel.java @@ -0,0 +1,42 @@ +package meerkat.comm; + +import com.google.protobuf.Message; +import meerkat.protobuf.Comm; + +/** + * A generic communication channel that supports point-to-point and broadcast operation + */ + +public interface Channel { + /** + * Return the id of the channel's endpoint (this will be used as the source of message sent from the channel). + * @return + */ + public int getSourceId(); + + public interface ReceiverCallback { + public void receiveMessage(Comm.BroadcastMessage envelope); + } + + /** + * sends a private message + * @param destUser destination user's identifier + * @param msg message + */ + public void sendMessage(int destUser, Message msg); + + + /** + * broadcasts a message to all parties (including the sender) + * @param msg message + */ + public void broadcastMessage(Message msg); + + /** + * Register a callback to handle received messages. + * The callback is called in the Channel thread, so no long processing should + * occur in the callback method. + * @param callback + */ + public void registerReceiverCallback(ReceiverCallback callback); +} diff --git a/meerkat-common/src/main/java/meerkat/comm/MessageInputStream.java b/meerkat-common/src/main/java/meerkat/comm/MessageInputStream.java new file mode 100644 index 0000000..b1e5255 --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/comm/MessageInputStream.java @@ -0,0 +1,98 @@ +package meerkat.comm; + +import com.google.protobuf.Message; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 21-Feb-16. + * A input stream of Protobuf messages + */ +public class MessageInputStream implements Iterable{ + + private T.Builder builder; + + private InputStream in; + + MessageInputStream(InputStream in, Class type) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + this.in = in; + this.builder = (T.Builder) type.getMethod("newBuilder").invoke(type); + } + + @Override + public Iterator iterator() { + + return new Iterator() { + + @Override + public boolean hasNext() { + try { + return isAvailable(); + } catch (IOException e) { + return false; + } + } + + @Override + public T next() { + try { + return readMessage(); + } catch (IOException e) { + return null; + } + } + + }; + + } + + /** + * Factory class for actually creating a MessageInputStream + */ + public static class MessageInputStreamFactory { + + public static MessageInputStream createMessageInputStream(InputStream in, Class type) + throws IOException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { + + return new MessageInputStream<>(in, type); + + } + + } + + public T readMessage() throws IOException{ + + builder.clear(); + builder.mergeDelimitedFrom(in); + return (T) builder.build(); + + } + + public boolean isAvailable() throws IOException { + return (in.available() > 0); + } + + public List asList() throws IOException{ + + List list = new LinkedList<>(); + + while (isAvailable()){ + list.add(readMessage()); + } + + return list; + + } + + public void close() throws IOException { + + in.close(); + + } + +} diff --git a/meerkat-common/src/main/java/meerkat/comm/MessageOutputStream.java b/meerkat-common/src/main/java/meerkat/comm/MessageOutputStream.java new file mode 100644 index 0000000..f72c04c --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/comm/MessageOutputStream.java @@ -0,0 +1,28 @@ +package meerkat.comm; + +import com.google.protobuf.Message; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Created by Arbel Deutsch Peled on 21-Feb-16. + * An output stream of Protobuf messages + */ +public class MessageOutputStream { + + private OutputStream out; + + public MessageOutputStream(OutputStream out) throws IOException { + this.out = out; + } + + public void writeMessage(T message) throws IOException { + message.writeDelimitedTo(out); + } + + public void close() throws IOException { + out.close(); + } + +} diff --git a/meerkat-common/src/main/java/meerkat/comm/Timestamp.java b/meerkat-common/src/main/java/meerkat/comm/Timestamp.java deleted file mode 100644 index 6c63854..0000000 --- a/meerkat-common/src/main/java/meerkat/comm/Timestamp.java +++ /dev/null @@ -1,7 +0,0 @@ -package meerkat.comm; - -/** - * Created by talm on 24/10/15. - */ -public class Timestamp { -} diff --git a/meerkat-common/src/main/java/meerkat/crypto/Digest.java b/meerkat-common/src/main/java/meerkat/crypto/Digest.java index 06b012c..e5202d6 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/Digest.java +++ b/meerkat-common/src/main/java/meerkat/crypto/Digest.java @@ -1,6 +1,7 @@ package meerkat.crypto; import com.google.protobuf.Message; +import meerkat.protobuf.BulletinBoardAPI.MessageID; import java.security.MessageDigest; @@ -13,7 +14,19 @@ public interface Digest { * (copied from {@link MessageDigest#digest()}) * @return */ - byte[] digest(); + public byte[] digest(); + + /** + * Completes the hash computation and returns a MessageID Protobuf as output + * @return + */ + public MessageID digestAsMessageID(); + + /** + * Updates the digest using the given raw data + * @param data contains the raw data + */ + public void update (byte[] data); /** * Updates the digest using the specified message (in serialized wire form) @@ -22,12 +35,12 @@ public interface Digest { * @param msg * @return */ - void update(Message msg); + public void update(Message msg); /** * Resets the digest for further use. */ - void reset(); + public void reset(); /** * Clone the current digest state diff --git a/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java index e7b64e5..707e500 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java +++ b/meerkat-common/src/main/java/meerkat/crypto/DigitalSignature.java @@ -5,11 +5,13 @@ import com.google.protobuf.Message; import java.io.IOException; import java.io.InputStream; -import java.security.InvalidKeyException; import java.security.KeyStore; -import java.security.SignatureException; -import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; +import java.security.SignatureException; +import java.security.InvalidKeyException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; import static meerkat.protobuf.Crypto.*; /** @@ -37,6 +39,13 @@ public interface DigitalSignature { */ public void clearVerificationCertificates(); + /** + * Add raw data to the content stream to be verified / signed. + * + * @param data + * @throws SignatureException + */ + public void updateContent(byte[] data) throws SignatureException; /** * Add msg to the content stream to be verified / signed. Each message is (automatically) @@ -71,6 +80,20 @@ public interface DigitalSignature { */ public boolean verify(); + /** + * Load a keystore from an input stream in PKCS12 format. + * + * @param keyStream + * @param password + * @return + * @throws IOException + * @throws CertificateException + * @throws KeyStoreException + * @throws NoSuchAlgorithmException + */ + public KeyStore.Builder getPKCS12KeyStoreBuilder(InputStream keyStream, char[] password) + throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException; + /** * Loads a private signing key. The keystore must include both the public and private * key parts. diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java index 6a33e49..e775226 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECDSASignature.java @@ -30,7 +30,10 @@ import javax.security.auth.callback.UnsupportedCallbackException; * * This class is not thread-safe (each thread should have its own instance). */ -public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignature { +public class ECDSASignature implements DigitalSignature { + + private static GlobalCryptoSetup globalCryptoSetup = GlobalCryptoSetup.getInstance(); + final Logger logger = LoggerFactory.getLogger(getClass()); final public static String KEYSTORE_TYPE = "PKCS12"; @@ -137,6 +140,11 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatur signer.update(msg.toByteString().asReadOnlyByteBuffer()); } + @Override + public void updateContent(byte[] data) throws SignatureException { + signer.update(data); + } + public void updateContent(InputStream in) throws IOException, SignatureException { ByteString inStr = ByteString.readFrom(in); signer.update(inStr.asReadOnlyByteBuffer()); @@ -208,6 +216,7 @@ public class ECDSASignature extends GlobalCryptoSetup implements DigitalSignatur * @throws KeyStoreException * @throws NoSuchAlgorithmException */ + @Override public KeyStore.Builder getPKCS12KeyStoreBuilder(InputStream keyStream, char[] password) throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException { KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE); diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECElGamalEncryption.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECElGamalEncryption.java index c0b5e73..31ad4c1 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/ECElGamalEncryption.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/ECElGamalEncryption.java @@ -31,7 +31,10 @@ import java.util.Random; /** * Created by talm on 17/11/15. */ -public class ECElGamalEncryption extends GlobalCryptoSetup implements Encryption { +public class ECElGamalEncryption implements Encryption { + + private static GlobalCryptoSetup globalCryptoSetup = GlobalCryptoSetup.getInstance(); + final Logger logger = LoggerFactory.getLogger(getClass()); public final static String KEY_ALGORITHM = "ECDH"; @@ -119,7 +122,7 @@ public class ECElGamalEncryption extends GlobalCryptoSetup implements Encryption Pair randomizer = elGamalPK.encrypt(curve.getInfinity(), rndInt); ConcreteCrypto.ElGamalCiphertext originalEncodedCipher= ConcreteCrypto.ElGamalCiphertext.parseFrom(msg.getData()); - Pair originalCipher = new Pair( + Pair originalCipher = new Pair( curve.decodePoint(originalEncodedCipher.getC1().toByteArray()), curve.decodePoint(originalEncodedCipher.getC2().toByteArray())); Pair newCipher = elGamalPK.add(originalCipher, randomizer); diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/GlobalCryptoSetup.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/GlobalCryptoSetup.java index 4f2e7a5..3d12701 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/GlobalCryptoSetup.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/GlobalCryptoSetup.java @@ -10,13 +10,26 @@ import java.security.Security; /** * A class that performs required crypto setup */ -public class GlobalCryptoSetup { +public final class GlobalCryptoSetup { + + private static GlobalCryptoSetup globalCryptoSetup; + final static Logger logger = LoggerFactory.getLogger(GlobalCryptoSetup.class); - static boolean loadedBouncyCastle = false; - static Provider bouncyCastleProvider; + private boolean loadedBouncyCastle = false; + private Provider bouncyCastleProvider; - public static boolean hasSecp256k1Curve() { + private GlobalCryptoSetup() { doSetup(); } + + public static GlobalCryptoSetup getInstance() { + if (globalCryptoSetup == null) { + globalCryptoSetup = new GlobalCryptoSetup(); + } + + return globalCryptoSetup; + } + + public boolean hasSecp256k1Curve() { // For now we just check if the java version is at least 8 String[] version = System.getProperty("java.version").split("\\."); int major = Integer.parseInt(version[0]); @@ -24,9 +37,11 @@ public class GlobalCryptoSetup { return ((major > 1) || ((major > 0) && (minor > 7))); } - public static Provider getBouncyCastleProvider() { doSetup(); return bouncyCastleProvider; } + public Provider getBouncyCastleProvider() { + return bouncyCastleProvider; + } - public static synchronized void doSetup() { + public void doSetup() { if (bouncyCastleProvider == null) { bouncyCastleProvider = new BouncyCastleProvider(); // Make bouncycastle our default provider if we're running on a JVM version < 8 @@ -39,5 +54,4 @@ public class GlobalCryptoSetup { } } - public GlobalCryptoSetup() { doSetup(); } } diff --git a/meerkat-common/src/main/java/meerkat/crypto/concrete/SHA256Digest.java b/meerkat-common/src/main/java/meerkat/crypto/concrete/SHA256Digest.java index 4f60af3..88f417c 100644 --- a/meerkat-common/src/main/java/meerkat/crypto/concrete/SHA256Digest.java +++ b/meerkat-common/src/main/java/meerkat/crypto/concrete/SHA256Digest.java @@ -3,6 +3,7 @@ package meerkat.crypto.concrete; import com.google.protobuf.ByteString; import com.google.protobuf.Message; import meerkat.crypto.Digest; +import meerkat.protobuf.BulletinBoardAPI.MessageID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,7 +14,7 @@ import java.security.NoSuchAlgorithmException; /** * Created by talm on 11/9/15. */ -public class SHA256Digest extends GlobalCryptoSetup implements Digest { +public class SHA256Digest implements Digest { final Logger logger = LoggerFactory.getLogger(getClass()); public static final String SHA256 = "SHA-256"; @@ -60,6 +61,11 @@ public class SHA256Digest extends GlobalCryptoSetup implements Digest { return hash.digest(); } + @Override + public MessageID digestAsMessageID() { + return MessageID.newBuilder().setID(ByteString.copyFrom(digest())).build(); + } + @Override public void update(Message msg) { @@ -74,6 +80,7 @@ public class SHA256Digest extends GlobalCryptoSetup implements Digest { hash.update(msg.asReadOnlyByteBuffer()); } + @Override final public void update(byte[] msg) { hash.update(msg); } diff --git a/meerkat-common/src/main/java/meerkat/pollingstation/PollingStationConstants.java b/meerkat-common/src/main/java/meerkat/pollingstation/PollingStationConstants.java new file mode 100644 index 0000000..1bc6d3c --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/pollingstation/PollingStationConstants.java @@ -0,0 +1,13 @@ +package meerkat.pollingstation; + +/** + * Created by Arbel Deutsch Peled on 21-Dec-15. + */ +public interface PollingStationConstants { + + // Relative addresses for Scanner operations + + public static final String POLLING_STATION_WEB_SCANNER_SCAN_PATH = "/scan"; + public static final String POLLING_STATION_WEB_SCANNER_ERROR_PATH = "/error"; + +} diff --git a/meerkat-common/src/main/java/meerkat/pollingstation/PollingStationScanner.java b/meerkat-common/src/main/java/meerkat/pollingstation/PollingStationScanner.java new file mode 100644 index 0000000..290d0ee --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/pollingstation/PollingStationScanner.java @@ -0,0 +1,61 @@ +package meerkat.pollingstation; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.protobuf.BoolValue; +import meerkat.protobuf.PollingStation.*; +/** + * Created by Arbel on 05/05/2016. + * An interface for the scanner used by the Polling Station Committee + * The scanner works as a producer, while the polling station is the consumer + * That is to say: scans are pushed from the scanner rather than requested by the polling station + */ +public interface PollingStationScanner { + + /** + * An interface for processing scans (Polling Station side) + */ + public interface Consumer { + + /** + * Sets up the connection to the scanner and begins receiving scans + * @throws Exception when the operation fails + */ + public void start() throws Exception; + + /** + * Closes the connection to the scanner + * @throws Exception when the operation fails + */ + public void stop() throws Exception; + + /** + * Subscribes to new scans + * + * @param scanCallback is the handler for scanned data + */ + public void subscribe(FutureCallback scanCallback); + + } + + /** + * An interface for submitting scanned data (scanner side) + */ + public interface Producer { + + /** + * Sends a scan to all subscribers + * @param scannedData contains the scanned data + * @return a BoolValue containing TRUE iff the scanned data has been sent to at least one subscriber + */ + public BoolValue newScan(ScannedData scannedData); + + /** + * Notifies subscribers about an error that occurred during scan + * @param errorMsg is the error that occurred + * @return a BoolValue containing TRUE iff the error has been sent to at least one subscriber + */ + public BoolValue reportScanError(ErrorMsg errorMsg); + + } + +} \ No newline at end of file diff --git a/meerkat-common/src/main/java/meerkat/util/BulletinBoardMessageComparator.java b/meerkat-common/src/main/java/meerkat/util/BulletinBoardMessageComparator.java index 77a6663..7005568 100644 --- a/meerkat-common/src/main/java/meerkat/util/BulletinBoardMessageComparator.java +++ b/meerkat-common/src/main/java/meerkat/util/BulletinBoardMessageComparator.java @@ -5,6 +5,7 @@ import meerkat.protobuf.BulletinBoardAPI.*; import meerkat.protobuf.Crypto.*; import java.util.Comparator; +import java.util.Iterator; import java.util.List; /** @@ -24,22 +25,55 @@ public class BulletinBoardMessageComparator implements Comparator msg1Sigs = msg1.getSigList(); - List msg2Sigs = msg2.getSigList(); - // Compare unsigned message - if (!msg1.getMsg().equals(msg2.getMsg())){ + + // Compare Timestamps + + if (!msg1.getMsg().getTimestamp().equals(msg2.getMsg().getTimestamp())){ return -1; } - // Compare signatures + // Compare tags (enforce order) - if (msg1Sigs.size() != msg2Sigs.size()){ + List tags1 = msg1.getMsg().getTagList(); + Iterator tags2 = msg2.getMsg().getTagList().iterator(); + + for (String tag : tags1){ + if (!tags2.hasNext()) { + return -1; + } + if (!tags2.next().equals(tag)){ + return -1; + } + } + + // Compare data + + if (msg1.getMsg().getDataTypeCase() != msg2.getMsg().getDataTypeCase()){ return -1; } - for (Signature sig : msg1Sigs){ - if (!msg2Sigs.contains(sig)) { + if (msg1.getMsg().getDataTypeCase() == UnsignedBulletinBoardMessage.DataTypeCase.DATA){ + if (!msg1.getMsg().getData().equals(msg2.getMsg().getData())){ + return -1; + } + } else if (msg1.getMsg().getDataTypeCase() == UnsignedBulletinBoardMessage.DataTypeCase.MSGID){ + if (!msg1.getMsg().getMsgId().equals(msg2.getMsg().getMsgId())){ + return -1; + } + } + + // Compare signatures (do not enforce order) + + List sigs1 = msg1.getSigList(); + List sigs2 = msg2.getSigList(); + + if (sigs1.size() != sigs2.size()){ + return -1; + } + + for (Signature sig : sigs1){ + if (!sigs2.contains(sig)) { return -1; } } diff --git a/meerkat-common/src/main/java/meerkat/util/BulletinBoardMessageGenerator.java b/meerkat-common/src/main/java/meerkat/util/BulletinBoardMessageGenerator.java new file mode 100644 index 0000000..5969a1e --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/util/BulletinBoardMessageGenerator.java @@ -0,0 +1,132 @@ +package meerkat.util; + +import com.google.protobuf.ByteString; +import meerkat.crypto.DigitalSignature; +import meerkat.protobuf.BulletinBoardAPI.*; +import com.google.protobuf.Timestamp; + +import java.math.BigInteger; +import java.security.SignatureException; +import java.util.LinkedList; +import java.util.List; +import java.util.Random; + +/** + * Created by Arbel Deutsch Peled on 21-Feb-16. + * This class contains methods used to generate random Bulletin Board Messages + */ +public class BulletinBoardMessageGenerator { + + private Random random; + + public BulletinBoardMessageGenerator(Random random) { + this.random = random; + } + + private byte randomByte(){ + return (byte) random.nextInt(); + } + + private byte[] randomBytes(int length) { + + byte[] result = new byte[length]; + + for (int i = 0; i < length; i++) { + result[i] = randomByte(); + } + + return result; + } + + private String randomString(){ + return new BigInteger(130, random).toString(32); + } + + private List randomStrings(int length) { + + List result = new LinkedList<>(); + + for (int i = 0; i < length; i++) { + result.add(randomString()); + } + + return result; + + } + + /** + * Generates a complete instance of a BulletinBoardMessage + * @param signers contains the (possibly multiple) credentials required to sign the message + * @param timestamp contains the time used in the message + * @param dataSize is the length of the data contained in the message + * @param tagNumber is the number of tags to generate + * @param tags is a list of initial tags (on top of which more will be added according to the method input) + * @return a random, signed Bulletin Board Message containing random data and tags and the given timestamp + */ + public BulletinBoardMessage generateRandomMessage(DigitalSignature[] signers, Timestamp timestamp, int dataSize, int tagNumber, List tags) + throws SignatureException{ + + // Generate random data. + + + + + + UnsignedBulletinBoardMessage unsignedMessage = + UnsignedBulletinBoardMessage.newBuilder() + .setData(ByteString.copyFrom(randomBytes(dataSize))) + .setTimestamp(timestamp) + .addAllTag(tags) + .addAllTag(randomStrings(tagNumber)) + .build(); + + BulletinBoardMessage.Builder messageBuilder = + BulletinBoardMessage.newBuilder() + .setMsg(unsignedMessage); + + for (int i = 0 ; i < signers.length ; i++) { + signers[i].updateContent(unsignedMessage); + messageBuilder.addSig(signers[i].sign()); + } + + return messageBuilder.build(); + + } + + /** + * Generates a complete instance of a BulletinBoardMessage + * @param signers contains the (possibly multiple) credentials required to sign the message + * @param timestamp contains the time used in the message + * @param dataSize is the length of the data contained in the message + * @param tagNumber is the number of tags to generate + * @return a random, signed Bulletin Board Message containing random data and tags and the given timestamp + */ + + public BulletinBoardMessage generateRandomMessage(DigitalSignature[] signers, Timestamp timestamp, int dataSize, int tagNumber) + throws SignatureException { + + List tags = new LinkedList<>(); + return generateRandomMessage(signers, timestamp, dataSize, tagNumber, tags); + + } + + /** + * Generates a complete instance of a BulletinBoardMessage + * @param signers contains the (possibly multiple) credentials required to sign the message + * @param dataSize is the length of the data contained in the message + * @param tagNumber is the number of tags to generate + * @return a random, signed Bulletin Board Message containing random data, tags and timestamp + */ + public BulletinBoardMessage generateRandomMessage(DigitalSignature[] signers, int dataSize, int tagNumber) + throws SignatureException { + + Timestamp timestamp = Timestamp.newBuilder() + .setSeconds(random.nextLong()) + .setNanos(random.nextInt()) + .build(); + + return generateRandomMessage(signers, timestamp, dataSize, tagNumber); + + } + +} diff --git a/meerkat-common/src/main/java/meerkat/util/BulletinBoardUtils.java b/meerkat-common/src/main/java/meerkat/util/BulletinBoardUtils.java new file mode 100644 index 0000000..a83f755 --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/util/BulletinBoardUtils.java @@ -0,0 +1,235 @@ +package meerkat.util; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Int64Value; +import meerkat.protobuf.BulletinBoardAPI.*; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/** + * Created by Arbel Deutsch Peled on 16-Feb-16. + */ +public class BulletinBoardUtils { + + /** + * Searches the tags in the message for one that begins with given prefix + * @param message is the message to search + * @param prefix is the given prefix + * @return the tag without the prefix, if found, or null if not found + */ + public static String findTagWithPrefix(BulletinBoardMessage message, String prefix) { + + for (String tag : message.getMsg().getTagList()){ + if (tag.startsWith(prefix)) { + return tag.substring(prefix.length()); + } + } + + return null; + + } + + /** + * Searches the tags in a message for tags that do not contain a given list of prefixes + * @param message is the message to search + * @param prefixes is the list of prefixes + * @return a list of the tags that do *not* contain any of the given prefixes + */ + public static List removePrefixTags(BulletinBoardMessage message, Iterable prefixes) { + + if (prefixes == null) + return message.getMsg().getTagList(); + + List result = new LinkedList<>(); + + for (String tag : message.getMsg().getTagList()){ + + boolean found = false; + + for (String prefix : prefixes){ + if (tag.startsWith(prefix)){ + found = true; + break; + } + } + + if (!found) { + result.add(tag); + } + + } + + return result; + + } + + /** + * This method creates a Timestamp Protobuf from a time specification + * @param timeInMillis is the time to encode since the Epoch time in milliseconds + * @return a Timestamp Protobuf encoding of the given time + */ + public static com.google.protobuf.Timestamp toTimestampProto(long timeInMillis) { + + return com.google.protobuf.Timestamp.newBuilder() + .setSeconds(timeInMillis / 1000) + .setNanos((int) ((timeInMillis % 1000) * 1000000)) + .build(); + + } + + /** + * This method creates a Timestamp Protobuf from the current system time + * @return a Timestamp Protobuf encoding of the current system time + */ + public static com.google.protobuf.Timestamp getCurrentTimestampProto() { + + return toTimestampProto(System.currentTimeMillis()); + + } + + /** + * This method converts an SQL Timestamp object into a Protobuf Timestamp object + * @param sqlTimestamp is the SQL Timestamp + * @return an equivalent Protobuf Timestamp + */ + public static com.google.protobuf.Timestamp toTimestampProto(java.sql.Timestamp sqlTimestamp) { + return toTimestampProto(sqlTimestamp.getTime()); + } + + /** + * This method converts a Protobuf Timestamp object into an SQL Timestamp object + * @param protoTimestamp is the Protobuf Timestamp + * @return an equivalent SQL Timestamp + */ + public static java.sql.Timestamp toSQLTimestamp(com.google.protobuf.Timestamp protoTimestamp) { + return new java.sql.Timestamp(protoTimestamp.getSeconds() * 1000 + protoTimestamp.getNanos() / 1000000); + } + + /** + * Breaks up a bulletin board message into chunks + * @param msg is the complete message + * @return a list of BatchChunks that contains the raw message data + */ + public static List breakToBatch(BulletinBoardMessage msg, int chunkSize) { + + byte[] data = msg.getMsg().getData().toByteArray(); + + int chunkNum = data.length / chunkSize; + if (data.length % chunkSize != 0) + chunkNum++; + + List chunkList = new ArrayList<>(chunkNum); + + int location = 0; + + for (int i=0 ; i < chunkNum ; i++) { + + int chunkLength; + + if (i == chunkNum - 1){ + chunkLength = data.length % chunkSize; + if (chunkLength == 0){ + chunkLength = chunkSize; + } + } else{ + chunkLength = chunkSize; + } + + chunkList.add(BatchChunk.newBuilder() + .setData(ByteString.copyFrom(data, location, chunkLength)) + .build()); + + location += chunkLength; + + } + + return chunkList; + + } + + /** + * Removes concrete data from the message and turns it into a stub + * Note that the stub does not contain the message ID + * Therefore, it cannot be used to retrieve the message from a server + * @param msg is the original message + * @return the message stub + */ + public static BulletinBoardMessage makeStub(BulletinBoardMessage msg) { + + return BulletinBoardMessage.newBuilder() + .mergeFrom(msg) + .setMsg(UnsignedBulletinBoardMessage.newBuilder() + .mergeFrom(msg.getMsg()) + .clearDataType() + .clearData() + .build()) + .build(); + + } + + /** + * Merges a batch chunk list back into a message stub to create a complete Bulletin Board message + * @param msgStub is a message stub + * @param chunkList contains the (ordered) data of the batch message + * @return a complete message containing both data and metadata + */ + public static BulletinBoardMessage gatherBatch(BulletinBoardMessage msgStub, List chunkList) { + + List dataList = new LinkedList<>(); + + for (BatchChunk chunk : chunkList){ + dataList.add(chunk.getData()); + } + + return BulletinBoardMessage.newBuilder() + .mergeFrom(msgStub) + .setMsg(UnsignedBulletinBoardMessage.newBuilder() + .mergeFrom(msgStub.getMsg()) + .setData(ByteString.copyFrom(dataList)) + .build()) + .build(); + + } + + /** + * Gerenates a BeginBatchMessage Protobuf which is used to begin uploading a message as a batch + * @param msg is the Bulletin Board message to be uploaded, which can be a stub or a complete message + * @return the required BeginBatchMessage + */ + public static BeginBatchMessage generateBeginBatchMessage(BulletinBoardMessage msg) { + + if (msg.getSigCount() <= 0){ + throw new IllegalArgumentException("No signatures found"); + } + + return BeginBatchMessage.newBuilder() + .addAllTag(msg.getMsg().getTagList()) + .build(); + + } + + /** + * Gerenates a CloseBatchMessage Protobuf which is used to finalize a batch message + * @param batchId is the temporary identifier for the message + * @param batchLength is the number of chunks in the batch + * @param msg is the Bulletin Board message that was uploaded (and can also be a stub of said message) + * @throws IllegalArgumentException if the message contains no signatures + */ + public static CloseBatchMessage generateCloseBatchMessage(Int64Value batchId, int batchLength, BulletinBoardMessage msg) { + + if (msg.getSigCount() <= 0){ + throw new IllegalArgumentException("No signatures found"); + } + + return CloseBatchMessage.newBuilder() + .setTimestamp(msg.getMsg().getTimestamp()) + .setBatchLength(batchLength) + .setBatchId(batchId.getValue()) + .addAllSig(msg.getSigList()) + .build(); + + } + +} diff --git a/meerkat-common/src/main/java/meerkat/util/TimestampComparator.java b/meerkat-common/src/main/java/meerkat/util/TimestampComparator.java new file mode 100644 index 0000000..9acdaa6 --- /dev/null +++ b/meerkat-common/src/main/java/meerkat/util/TimestampComparator.java @@ -0,0 +1,31 @@ +package meerkat.util; + +import com.google.protobuf.Timestamp; + +import java.util.Comparator; + +/** + * Created by Arbel Deutsch Peled on 20-Feb-16. + */ +public class TimestampComparator implements Comparator { + + @Override + public int compare(Timestamp o1, Timestamp o2) { + + if (o1.getSeconds() != o2.getSeconds()){ + + return o1.getSeconds() > o2.getSeconds() ? 2 : -2; + + } else if (o1.getNanos() != o2.getNanos()){ + + return o1.getNanos() > o2.getNanos() ? 1 : -1; + + } else{ + + return 0; + + } + + } + +} diff --git a/meerkat-common/src/main/proto/meerkat/BulletinBoardAPI.proto b/meerkat-common/src/main/proto/meerkat/BulletinBoardAPI.proto index 2e2e7e2..f5f4bf5 100644 --- a/meerkat-common/src/main/proto/meerkat/BulletinBoardAPI.proto +++ b/meerkat-common/src/main/proto/meerkat/BulletinBoardAPI.proto @@ -5,6 +5,7 @@ package meerkat; option java_package = "meerkat.protobuf"; import 'meerkat/crypto.proto'; +import 'google/protobuf/timestamp.proto'; message BoolMsg { bool value = 1; @@ -21,15 +22,28 @@ message MessageID { } message UnsignedBulletinBoardMessage { - // Optional tags describing message + // Optional tags describing message; Used for message retrieval repeated string tag = 1; - // The actual content of the message - bytes data = 2; + // Timestamp of the message (as defined by client) + google.protobuf.Timestamp timestamp = 2; + + // The payload of the message + oneof dataType{ + + // A unique message identifier + bytes msgId = 3; + + // The actual content of the message + bytes data = 4; + + } + + } message BulletinBoardMessage { - + // Serial entry number of message in database int64 entryNum = 1; @@ -38,70 +52,131 @@ message BulletinBoardMessage { // Signature of message (and tags), excluding the entry number. repeated meerkat.Signature sig = 3; + } message BulletinBoardMessageList { - + repeated BulletinBoardMessage message = 1; - + } enum FilterType { MSG_ID = 0; // Match exact message ID EXACT_ENTRY = 1; // Match exact entry number in database (chronological) MAX_ENTRY = 2; // Find all entries in database up to specified entry number (chronological) - SIGNER_ID = 3; // Find all entries in database that correspond to specific signature (signer) - TAG = 4; // Find all entries in database that have a specific tag + MIN_ENTRY = 3; // Find all entries in database starting from specified entry number (chronological) + SIGNER_ID = 4; // Find all entries in database that correspond to specific signature (signer) + TAG = 5; // Find all entries in database that have a specific tag + AFTER_TIME = 6; // Find all entries in database that occurred on or after a given timestamp + BEFORE_TIME = 7; // Find all entries in database that occurred on or before a given timestamp // NOTE: The MAX_MESSAGES filter must remain the last filter type // This is because the condition it specifies in an SQL statement must come last in the statement // Keeping it last here allows for easily sorting the filters and keeping the code general - MAX_MESSAGES = 5; // Return at most some specified number of messages + MAX_MESSAGES = 8; // Return at most some specified number of messages } message MessageFilter { - + FilterType type = 1; - + oneof filter{ bytes id = 2; int64 entry = 3; string tag = 4; int64 maxMessages = 5; + google.protobuf.Timestamp timestamp = 6; } } message MessageFilterList { - + // Combination of filters. // To be implemented using intersection ("AND") operations. repeated MessageFilter filter = 1; - + } // This message is used to start a batch transfer to the Bulletin Board Server message BeginBatchMessage { - bytes signerId = 1; // Unique signer identifier - int32 batchId = 2; // Unique identifier for the batch (unique per signer) - repeated string tag = 3; // Tags for the batch message + repeated string tag = 1; // Tags for the batch message } // This message is used to finalize and sign a batch transfer to the Bulletin Board Server message CloseBatchMessage { - int32 batchId = 1; // Unique identifier for the batch (unique per signer) - int32 batchLength = 2; // Number of messages in the batch - meerkat.Signature sig = 3; // Signature on the (ordered) batch messages + int64 batchId = 1; // Unique temporary identifier for the batch + int32 batchLength = 2; // Number of messages in the batch + google.protobuf.Timestamp timestamp = 3; // Timestamp of the batch (as defined by client) + repeated meerkat.Signature sig = 4; // Signatures on the (ordered) batch messages } -// Container for single batch message data -message BatchData { +// Container for single chunk of abatch message +message BatchChunk { bytes data = 1; } +// List of BatchChunk; Only used for testing +message BatchChunkList { + repeated BatchChunk data = 1; +} + // These messages comprise a batch message message BatchMessage { - bytes signerId = 1; // Unique signer identifier - int32 batchId = 2; // Unique identifier for the batch (unique per signer) - int32 serialNum = 3; // Location of the message in the batch: starting from 0 - BatchData data = 4; // Actual data + int64 batchId = 1; // Unique temporary identifier for the batch + int32 serialNum = 2; // Location of the message in the batch: starting from 0 + BatchChunk data = 3; // Actual data +} + +// This message is used to define a single query to the server to ascertain whether or not the server is synched with the client +// up till a specified timestamp +message SingleSyncQuery { + + google.protobuf.Timestamp timeOfSync = 1; + int64 checksum = 2; + +} + +// This message defines a complete server sync query +message SyncQuery { + + MessageFilterList filterList = 1; + + repeated SingleSyncQuery query = 2; + +} + +// This message defines the required information for generation of a SyncQuery instance by the server +message GenerateSyncQueryParams { + + // Defines the set of messages required + MessageFilterList filterList = 1; + + // Defines the locations in the list of messages to calculate single sync queries for + // The values should be between 0.0 and 1.0 and define the location in fractions of the size of the message set + repeated float breakpointList = 2; + +} + +// This message defines the server's response format to a sync query +message SyncQueryResponse { + + // Serial entry number of current last entry in database + // Set to zero (0) in case no query checksums match + int64 lastEntryNum = 1; + + // Largest value of timestamp for which the checksums match + google.protobuf.Timestamp lastTimeOfSync = 2; + +} + +// This message defines a query for retrieval of batch data +message BatchQuery { + + // The unique message ID if the batch + MessageID msgID = 1; + + // The first chunk to retrieve (0 is the first chunk) + int32 startPosition = 2; + } \ No newline at end of file diff --git a/meerkat-common/src/main/proto/meerkat/PollingStation.proto b/meerkat-common/src/main/proto/meerkat/PollingStation.proto new file mode 100644 index 0000000..09596a5 --- /dev/null +++ b/meerkat-common/src/main/proto/meerkat/PollingStation.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package meerkat; + +import "meerkat/voting.proto"; + +option java_package = "meerkat.protobuf"; + +// Container for scanned data +message ScannedData { + bytes channel = 1; + + SignedEncryptedBallot signed_encrypted_ballot = 2; + +} + +// Container for error messages +message ErrorMsg { + string msg = 1; +} \ No newline at end of file diff --git a/meerkat-common/src/main/proto/meerkat/comm.proto b/meerkat-common/src/main/proto/meerkat/comm.proto new file mode 100644 index 0000000..6808288 --- /dev/null +++ b/meerkat-common/src/main/proto/meerkat/comm.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package meerkat; + +option java_package = "meerkat.protobuf"; + +message BroadcastMessage { + int32 sender = 1; + int32 destination = 2; + bool is_private = 3; + + bytes payload = 5; +} \ No newline at end of file diff --git a/meerkat-common/src/main/proto/meerkat/voting.proto b/meerkat-common/src/main/proto/meerkat/voting.proto index 9837cce..6e03a5b 100644 --- a/meerkat-common/src/main/proto/meerkat/voting.proto +++ b/meerkat-common/src/main/proto/meerkat/voting.proto @@ -6,11 +6,49 @@ import 'meerkat/crypto.proto'; option java_package = "meerkat.protobuf"; -// A ballot question. This is an opaque -// data type that is parsed by the UI to display -// the question. + + +// Type of the element data to be presented by UI +enum UIElementDataType { + TEXT = 0; + IMAGE = 1; + VOICE = 2; +} + +// Type of question +enum QuestionType { + MULTIPLE_CHOICE = 0; + MULTIPLE_SELECTION = 1; + ORDER = 2; +} + +// An element to be presented by UI +message UIElement { + UIElementDataType type = 1; + bytes data = 2; +} + +// A question in the ballot +// is_mandatory determines whether the question may be skipped with no answer +// description might hold information/guidlines for the voter message BallotQuestion { - bytes data = 1; + bool is_mandatory = 1; + UIElement question = 2; + UIElement description = 3; + repeated UIElement answer = 4; +} + + +message QuestionCluster { + UIElement cluster_description = 1; + repeated int32 question_index = 2; + +} + +message Channel { + UIElement channel_description = 1; + repeated int32 cluster_index = 2; + } // An answer to a specific ballot question. @@ -22,17 +60,22 @@ message BallotAnswer { } message PlaintextBallot { - uint64 serialNumber = 1; // Ballot serial number - - repeated BallotAnswer answers = 2; + uint64 serial_number = 1; // Ballot serial number + bytes channel_identifier = 2; + repeated BallotAnswer answers = 3; } message EncryptedBallot { - uint64 serialNumber = 1; // Ballot serial number + uint64 serial_number = 1; // Ballot serial number RerandomizableEncryptedMessage data = 2; } +message SignedEncryptedBallot { + EncryptedBallot encrypted_ballot = 1; + Signature signature = 2; +} + message BallotSecrets { PlaintextBallot plaintext_ballot = 1; @@ -45,6 +88,10 @@ message BoothParams { } +message BoothSystemMessages { + map system_message = 1; +} + // A table to translate to and from compactly encoded answers // and their human-understandable counterparts. // This should be parsable by the UI @@ -79,12 +126,29 @@ message ElectionParams { // How many mixers must participate for the mixing to be considered valid uint32 mixerThreshold = 5; - // Candidate list (or other question format) - repeated BallotQuestion questions = 6; + // questions to first indicate the voter's channel + repeated BallotQuestion channel_choice_questions = 6; - // Translation table between answers and plaintext encoding - BallotAnswerTranslationTable answerTranslationTable = 7; + // translating the channel-choice answers to the voter's channel + SimpleCategoriesSelectionData selection_data = 7; + + // Candidate list (or other question format) + repeated BallotQuestion race_questions = 8; // Data required in order to access the Bulletin Board Servers - BulletinBoardClientParams bulletinBoardClientParams = 8; + BulletinBoardClientParams bulletinBoardClientParams = 9; } + +message Category { + repeated uint32 questionIndex = 1; +} + +message CategoryChooser { + repeated Category category = 1; +} + +message SimpleCategoriesSelectionData { + Category shared_defaults = 1; + repeated CategoryChooser categoryChooser = 2; +} + diff --git a/meerkat-common/src/test/java/meerkat/bulletinboard/BulletinBoardDigestTest.java b/meerkat-common/src/test/java/meerkat/bulletinboard/BulletinBoardDigestTest.java new file mode 100644 index 0000000..277bda1 --- /dev/null +++ b/meerkat-common/src/test/java/meerkat/bulletinboard/BulletinBoardDigestTest.java @@ -0,0 +1,112 @@ +package meerkat.bulletinboard; + +import com.google.protobuf.ByteString; +import meerkat.crypto.concrete.ECDSASignature; +import meerkat.crypto.concrete.SHA256Digest; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.util.BulletinBoardMessageGenerator; +import meerkat.util.BulletinBoardUtils; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.security.*; +import java.security.cert.CertificateException; +import java.util.List; +import java.util.Random; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.fail; + +/** + * Created by Arbel Deutsch Peled on 16-Jun-16. + */ +public class BulletinBoardDigestTest { + + private static String KEYFILE_EXAMPLE = "/certs/enduser-certs/user1-key-with-password-secret.p12"; + private static String KEYFILE_EXAMPLE3 = "/certs/enduser-certs/user3-key-with-password-shh.p12"; + + private static String KEYFILE_PASSWORD1 = "secret"; + private static String KEYFILE_PASSWORD3 = "shh"; + + private GenericBulletinBoardSignature[] signers; + private ByteString[] signerIDs; + + @Before + public void init() { + + signers = new GenericBulletinBoardSignature[2]; + signerIDs = new ByteString[signers.length]; + signers[0] = new GenericBulletinBoardSignature(new ECDSASignature()); + signers[1] = new GenericBulletinBoardSignature(new ECDSASignature()); + + InputStream keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE); + char[] password = KEYFILE_PASSWORD1.toCharArray(); + + KeyStore.Builder keyStoreBuilder = null; + try { + keyStoreBuilder = signers[0].getPKCS12KeyStoreBuilder(keyStream, password); + + signers[0].loadSigningCertificate(keyStoreBuilder); + + keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE3); + password = KEYFILE_PASSWORD3.toCharArray(); + + keyStoreBuilder = signers[1].getPKCS12KeyStoreBuilder(keyStream, password); + signers[1].loadSigningCertificate(keyStoreBuilder); + + for (int i = 0 ; i < signers.length ; i++) { + signerIDs[i] = signers[i].getSignerID(); + } + + } catch (IOException e) { + System.err.println("Failed reading from signature file " + e.getMessage()); + fail("Failed reading from signature file " + e.getMessage()); + } catch (CertificateException e) { + System.err.println("Failed reading certificate " + e.getMessage()); + fail("Failed reading certificate " + e.getMessage()); + } catch (KeyStoreException e) { + System.err.println("Failed reading keystore " + e.getMessage()); + fail("Failed reading keystore " + e.getMessage()); + } catch (NoSuchAlgorithmException e) { + System.err.println("Couldn't find signing algorithm " + e.getMessage()); + fail("Couldn't find signing algorithm " + e.getMessage()); + } catch (UnrecoverableKeyException e) { + System.err.println("Couldn't find signing key " + e.getMessage()); + fail("Couldn't find signing key " + e.getMessage()); + } + + } + + @Test + public void testBatchDigest() throws SignatureException { + + final int MESSAGE_SIZE = 100; + final int CHUNK_SIZE = 10; + final int TAG_NUM = 10; + + BulletinBoardMessageGenerator generator = new BulletinBoardMessageGenerator(new Random(0)); + + BulletinBoardMessage completeMessage = generator.generateRandomMessage(signers, MESSAGE_SIZE, TAG_NUM); + + BulletinBoardMessage stub = BulletinBoardUtils.makeStub(completeMessage); + List batchChunks = BulletinBoardUtils.breakToBatch(completeMessage, CHUNK_SIZE); + + BulletinBoardDigest digest = new GenericBulletinBoardDigest(new SHA256Digest()); + + digest.update(completeMessage); + MessageID id1 = digest.digestAsMessageID(); + + digest.update(stub); + for (BatchChunk batchChunk : batchChunks){ + digest.update(batchChunk.getData().toByteArray()); + } + + MessageID id2 = digest.digestAsMessageID(); + + assertThat("Digests not equal!", id1.getID().equals(id2.getID())); + + } + +} diff --git a/meerkat-common/src/test/java/meerkat/comm/ChannelImpl.java b/meerkat-common/src/test/java/meerkat/comm/ChannelImpl.java new file mode 100644 index 0000000..cfe32c2 --- /dev/null +++ b/meerkat-common/src/test/java/meerkat/comm/ChannelImpl.java @@ -0,0 +1,96 @@ +package meerkat.comm; + +import com.google.protobuf.Message; +import com.google.protobuf.TextFormat; +import meerkat.protobuf.Comm; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; +import java.util.TreeMap; +import java.util.concurrent.*; + +/** + * Created by Tzlil on 2/14/2016. + */ +// TODO: Change nane to network + +public class ChannelImpl { + final Logger logger = LoggerFactory.getLogger(getClass()); + + //private ExecutorService executorService = Executors.newCachedThreadPool(); + public static int BROADCAST = 0; + Map channels = new TreeMap<>(); + + + public ChannelImpl() { + } + + public Channel getChannel(int id) { + return new SingleChannel(id); + } + + public class SingleChannel implements Channel { + protected final int id; + + ReceiverCallback callback; + + SingleChannel(int id) { + this.id = id; + channels.put(id, this); + } + + @Override + public int getSourceId() { + return id; + } + + + @Override + public void sendMessage(int destUser, Message msg) { + if (destUser < 1) + return; + SingleChannel channel = channels.get(destUser); + if (channel == null) { + logger.warn("Party {} attempting to send message to non-existing party {}", getSourceId(), destUser); + return; + } + Comm.BroadcastMessage broadcastMessage = Comm.BroadcastMessage.newBuilder() + .setSender(id) + .setDestination(destUser) + .setIsPrivate(true) + .setPayload(msg.toByteString()) + .build(); + + logger.debug("sending Message: Dst={},Src={} [{}]", broadcastMessage.getDestination(), + broadcastMessage.getSender(), TextFormat.printToString(msg)); + + channel.callback.receiveMessage(broadcastMessage); + } + + @Override + public void broadcastMessage(Message msg) { + Comm.BroadcastMessage broadcastMessage = Comm.BroadcastMessage.newBuilder() + .setSender(id) + .setDestination(BROADCAST) + .setIsPrivate(false) + .setPayload(msg.toByteString()) + .build(); + + logger.debug("broadcasting Message: Src={} [{}]", + broadcastMessage.getSender(), TextFormat.printToString(msg)); + + for (SingleChannel channel : channels.values()) { + channel.callback.receiveMessage(broadcastMessage); + } + } + + @Override + public void registerReceiverCallback(final ReceiverCallback callback) { + this.callback = callback; + } + } + +} diff --git a/meerkat-common/src/test/java/meerkat/comm/MessageStreamTest.java b/meerkat-common/src/test/java/meerkat/comm/MessageStreamTest.java new file mode 100644 index 0000000..58dc7c1 --- /dev/null +++ b/meerkat-common/src/test/java/meerkat/comm/MessageStreamTest.java @@ -0,0 +1,98 @@ +package meerkat.comm; + +import com.google.protobuf.*; +import meerkat.comm.MessageInputStream.MessageInputStreamFactory; +import meerkat.protobuf.BulletinBoardAPI.*; +import meerkat.protobuf.Crypto; +import meerkat.util.BulletinBoardMessageComparator; +import org.junit.Test; + +import java.io.*; +import java.lang.reflect.InvocationTargetException; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; + +/** + * Created by Arbel Deutsch Peled on 21-Feb-16. + * Tests for MessageInputStream and MessageOutputStream classes + */ +public class MessageStreamTest { + + @Test + public void testWithBulletinBoardMessages() { + + MessageOutputStream out; + MessageInputStream in; + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + BulletinBoardMessageComparator comparator = new BulletinBoardMessageComparator(); + + byte[] b1 = {(byte) 1, (byte) 2, (byte) 3, (byte) 4}; + byte[] b2 = {(byte) 11, (byte) 12, (byte) 13, (byte) 14}; + byte[] b3 = {(byte) 21, (byte) 22, (byte) 23, (byte) 24}; + + try { + + out = new MessageOutputStream<>(stream); + + } catch (IOException e) { + + System.err.println(e.getMessage()); + assertThat("Error creating streams: " + e.getMessage(), false); + return; + + } + + + + BulletinBoardMessage message = BulletinBoardMessage.newBuilder() + .setEntryNum(1) + .setMsg(UnsignedBulletinBoardMessage.newBuilder() + .setData(ByteString.copyFrom(b1)) + .addTag("Test") + .addTag("1234") + .setTimestamp(com.google.protobuf.Timestamp.newBuilder() + .setSeconds(19823451) + .setNanos(2134) + .build()) + .build()) + .addSig(Crypto.Signature.newBuilder() + .setSignerId(ByteString.copyFrom(b2)) + .setData(ByteString.copyFrom(b3)) + .build()) + .build(); + + try { + + out.writeMessage(message); + + } catch (IOException e) { + + System.err.println(e.getMessage()); + assertThat("Error writing message: " + e.getMessage(), false); + + } + + try { + + in = MessageInputStreamFactory.createMessageInputStream( + new ByteArrayInputStream(stream.toByteArray()), + BulletinBoardMessage.class); + + assertThat("Retrieved message was not identical to send message", comparator.compare(message, in.readMessage()), is(equalTo(0))); + + } catch (IOException e) { + + System.err.println(e.getMessage()); + assertThat("Error reading message: " + e.getMessage(), false); + + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + System.err.println(e.getMessage()); + assertThat("Error creating input stream " + e.getMessage(), false); + } + + } + +} diff --git a/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSADeterministicSignatureTest.java b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSADeterministicSignatureTest.java index cc41497..4baa741 100644 --- a/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSADeterministicSignatureTest.java +++ b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSADeterministicSignatureTest.java @@ -3,7 +3,6 @@ package meerkat.crypto.concrete; import com.google.protobuf.ByteString; import com.google.protobuf.Message; -import meerkat.crypto.concrete.ECDSASignature; import meerkat.protobuf.Crypto; import org.junit.Test; diff --git a/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSASignatureTest.java b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSASignatureTest.java index 37c2d1b..e5b448d 100644 --- a/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSASignatureTest.java +++ b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECDSASignatureTest.java @@ -3,7 +3,6 @@ package meerkat.crypto.concrete; import com.google.protobuf.ByteString; import com.google.protobuf.Message; import meerkat.protobuf.Crypto; -import meerkat.crypto.concrete.ECDSASignature; import org.junit.Before; import org.junit.Test; diff --git a/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalUtils.java b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalUtils.java index 4322aad..e26e00b 100644 --- a/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalUtils.java +++ b/meerkat-common/src/test/java/meerkat/crypto/concrete/ECElGamalUtils.java @@ -26,6 +26,9 @@ import java.security.spec.InvalidKeySpecException; * utilities for ECElgamal */ public class ECElGamalUtils { + + private static GlobalCryptoSetup globalCryptoSetup = GlobalCryptoSetup.getInstance(); + final static Logger logger = LoggerFactory.getLogger(ECElGamalUtils.class); public final static String ENCRYPTION_KEY_ALGORITHM = "ECDH"; @@ -43,7 +46,7 @@ public class ECElGamalUtils { try { KeyFactory fact = KeyFactory.getInstance(ENCRYPTION_KEY_ALGORITHM, - GlobalCryptoSetup.getBouncyCastleProvider()); + globalCryptoSetup.getBouncyCastleProvider()); PublicKey javaPk = fact.generatePublic(pubKeySpec); ConcreteCrypto.ElGamalPublicKey serializedPk = ConcreteCrypto.ElGamalPublicKey.newBuilder() .setSubjectPublicKeyInfo(ByteString.copyFrom(javaPk.getEncoded())).build(); @@ -77,7 +80,7 @@ public class ECElGamalUtils { try { java.lang.reflect.Method newBuilder = plaintextMessageType.getMethod("newBuilder"); - GeneratedMessage.Builder builder = (GeneratedMessage.Builder) newBuilder.invoke(plaintextMessageType); + Message.Builder builder = (Message.Builder) newBuilder.invoke(plaintextMessageType); builder.mergeDelimitedFrom(in); return plaintextMessageType.cast(builder.build()); } catch (Exception e) { diff --git a/meerkat-common/src/test/resources/certs/enduser-certs/user3-key-with-password-shh.p12 b/meerkat-common/src/test/resources/certs/enduser-certs/user3-key-with-password-shh.p12 new file mode 100644 index 0000000..c62cee1 Binary files /dev/null and b/meerkat-common/src/test/resources/certs/enduser-certs/user3-key-with-password-shh.p12 differ diff --git a/meerkat-common/src/test/resources/certs/enduser-certs/user3.crt b/meerkat-common/src/test/resources/certs/enduser-certs/user3.crt new file mode 100644 index 0000000..23ee857 --- /dev/null +++ b/meerkat-common/src/test/resources/certs/enduser-certs/user3.crt @@ -0,0 +1,8 @@ +-----BEGIN CERTIFICATE----- +MIIBGjCBw6ADAgECAgEBMAkGByqGSM49BAEwEDEOMAwGA1UEAxMFY2VydDEwHhcN +MTUxMTI4MTEwNDAwWhcNMTYxMTI4MTEwNDAwWjAQMQ4wDAYDVQQDEwVjZXJ0MTBZ +MBMGByqGSM49AgEGCCqGSM49AwEHA0IABLiyFMVWQtFi4fCjOGLDwQcdjyr48Y8j +P+eLEIGMYKKv8bqL3Vchs0iOPoyGH6jxYj2/ShnLSIEuIMPfVgV9kxSjDzANMAsG +A1UdDwQEAwIHgDAJBgcqhkjOPQQBA0cAMEQCIH7R0AWO0AYiHOs+QsHEpWiebFc1 +cyxCKJGkf8KA1KJrAiArCia7PWl0KzaqA0RQC4J0BKp4rZo1PCqKI8DirKQf/Q== +-----END CERTIFICATE----- diff --git a/mixer/src/main/java/meerkat/mixer/main/BatchConverter.java b/mixer/src/main/java/meerkat/mixer/main/BatchConverter.java index f2d02dd..1ad8204 100644 --- a/mixer/src/main/java/meerkat/mixer/main/BatchConverter.java +++ b/mixer/src/main/java/meerkat/mixer/main/BatchConverter.java @@ -51,24 +51,24 @@ public class BatchConverter { * @param mixerOutput * @return meerkat.mixer.mixing output as list of batch data */ - public List MixerOutput2BatchData(MixerOutput mixerOutput) { + public List MixerOutput2BatchChunk(MixerOutput mixerOutput) { - List result = new ArrayList(); + List result = new ArrayList(); - result.add(BulletinBoardAPI.BatchData.newBuilder() + result.add(BulletinBoardAPI.BatchChunk.newBuilder() .setData(Integer2ByteString(n)) .build()); for (Mixing.ZeroKnowledgeProof[] zkpLayer : mixerOutput.getProofs()) { for (Mixing.ZeroKnowledgeProof zkp : zkpLayer) { - result.add(BulletinBoardAPI.BatchData.newBuilder() + result.add(BulletinBoardAPI.BatchChunk.newBuilder() .setData(zkp.toByteString()) .build()); } } for (Crypto.RerandomizableEncryptedMessage[] encryptionLayer : mixerOutput.getEncryptedMessages()) { for (Crypto.RerandomizableEncryptedMessage encryption : encryptionLayer) { - result.add(BulletinBoardAPI.BatchData.newBuilder() + result.add(BulletinBoardAPI.BatchChunk.newBuilder() .setData(encryption.toByteString()) .build()); } @@ -78,14 +78,14 @@ public class BatchConverter { /** * convert batch data list to meerkat.mixer.mixing output - * @param batchDataList + * @param batchChunkList * @return batch data list as MixerOutput * @throws Exception */ - public MixerOutput BatchDataList2MixerOutput - (List batchDataList) throws Exception { + public MixerOutput BatchChunkList2MixerOutput + (List batchChunkList) throws Exception { - if (n != ByteString2Integer(batchDataList.remove(0).getData())){ + if (n != ByteString2Integer(batchChunkList.remove(0).getData())){ throw new Exception(); } @@ -95,7 +95,7 @@ public class BatchConverter { { for (int proofIndex = 0 ; proofIndex < nDiv2 ; proofIndex ++) { - proofs[layer][proofIndex] = Mixing.ZeroKnowledgeProof.parseFrom(batchDataList.remove(0).getData()); + proofs[layer][proofIndex] = Mixing.ZeroKnowledgeProof.parseFrom(batchChunkList.remove(0).getData()); } } @@ -106,7 +106,7 @@ public class BatchConverter { for (int encryptionIndex = 0 ; encryptionIndex < n ; encryptionIndex ++) { encryptions[layer][encryptionIndex] = Crypto.RerandomizableEncryptedMessage - .parseFrom(batchDataList.remove(0).getData()); + .parseFrom(batchChunkList.remove(0).getData()); } } diff --git a/mixer/src/main/java/meerkat/mixer/main/BatchHandler.java b/mixer/src/main/java/meerkat/mixer/main/BatchHandler.java index e609234..8356475 100644 --- a/mixer/src/main/java/meerkat/mixer/main/BatchHandler.java +++ b/mixer/src/main/java/meerkat/mixer/main/BatchHandler.java @@ -71,7 +71,7 @@ public class BatchHandler implements AsyncBulletinBoardClient.ClientCallback callback) { BatchConverter batchConverter = new BatchConverter(n,layers); - List batchDataList = batchConverter.MixerOutput2BatchData(mixerOutput); - asyncBulletinBoardClient.postBatch(id, batchId, batchDataList, callback); + List batchChunkList = batchConverter.MixerOutput2BatchChunk(mixerOutput); + asyncBulletinBoardClient.postBatch(id, batchId, batchChunkList, callback); } - - - } diff --git a/mixer/src/main/java/meerkat/mixer/necessary/AsyncBulletinBoardClient.java b/mixer/src/main/java/meerkat/mixer/necessary/AsyncBulletinBoardClient.java index d1a2d9f..208c7c7 100644 --- a/mixer/src/main/java/meerkat/mixer/necessary/AsyncBulletinBoardClient.java +++ b/mixer/src/main/java/meerkat/mixer/necessary/AsyncBulletinBoardClient.java @@ -32,17 +32,17 @@ public interface AsyncBulletinBoardClient extends BulletinBoardClient { * This method allows for sending large messages as a batch to the bulletin board * @param signerId is the canonical form for the ID of the sender of this batch * @param batchId is a unique (per signer) ID for this batch - * @param batchDataList is the (canonically ordered) list of data comprising the batch message - * @param startPosition is the location (in the batch) of the first entry in batchDataList (optionally used to continue interrupted post operations) + * @param batchChunkList is the (canonically ordered) list of data comprising the batch message + * @param startPosition is the location (in the batch) of the first entry in batchChunkList (optionally used to continue interrupted post operations) * @param callback is a callback function class for handling results of the operation * @return a unique message ID for the entire message, that can be later used to retrieve the batch */ - public MessageID postBatch(byte[] signerId, int batchId, List batchDataList, int startPosition, ClientCallback callback); + public MessageID postBatch(byte[] signerId, int batchId, List batchChunkList, int startPosition, ClientCallback callback); /** * Overloading of the postBatch method in which startPosition is set to the default value 0 */ - public MessageID postBatch(byte[] signerId, int batchId, List batchDataList, ClientCallback callback); + public MessageID postBatch(byte[] signerId, int batchId, List batchChunkList, ClientCallback callback); /** * Check how "safe" a given message is in an asynchronous manner diff --git a/mixer/src/main/java/meerkat/mixer/necessary/CompleteBatch.java b/mixer/src/main/java/meerkat/mixer/necessary/CompleteBatch.java index c93506e..4e18d43 100644 --- a/mixer/src/main/java/meerkat/mixer/necessary/CompleteBatch.java +++ b/mixer/src/main/java/meerkat/mixer/necessary/CompleteBatch.java @@ -17,11 +17,11 @@ import java.util.List; public class CompleteBatch { private BeginBatchMessage beginBatchMessage; - private List batchDataList; + private List batchChunkList; private Signature signature; public CompleteBatch() { - batchDataList = new LinkedList(); + batchChunkList = new LinkedList(); } public CompleteBatch(BeginBatchMessage newBeginBatchMessage) { @@ -29,13 +29,13 @@ public class CompleteBatch { beginBatchMessage = newBeginBatchMessage; } - public CompleteBatch(BeginBatchMessage newBeginBatchMessage, List newDataList) { + public CompleteBatch(BeginBatchMessage newBeginBatchMessage, List newChunkList) { this(newBeginBatchMessage); - appendBatchData(newDataList); + appendBatchChunk(newChunkList); } - public CompleteBatch(BeginBatchMessage newBeginBatchMessage, List newDataList, Signature newSignature) { - this(newBeginBatchMessage, newDataList); + public CompleteBatch(BeginBatchMessage newBeginBatchMessage, List newChunkList, Signature newSignature) { + this(newBeginBatchMessage, newChunkList); signature = newSignature; } @@ -43,8 +43,8 @@ public class CompleteBatch { return beginBatchMessage; } - public List getBatchDataList() { - return batchDataList; + public List getBatchChunkList() { + return batchChunkList; } public Signature getSignature() { @@ -55,12 +55,12 @@ public class CompleteBatch { this.beginBatchMessage = beginBatchMessage; } - public void appendBatchData(BatchData newBatchData) { - batchDataList.add(newBatchData); + public void appendBatchChunk(BatchChunk newBatchChunk) { + batchChunkList.add(newBatchChunk); } - public void appendBatchData(List newBatchDataList) { - batchDataList.addAll(newBatchDataList); + public void appendBatchChunk(List newBatchChunkList) { + batchChunkList.addAll(newBatchChunkList); } public void setSignature(Signature newSignature) { diff --git a/mixer/src/test/java/meerkat/mixer/RerandomizeTest.java b/mixer/src/test/java/meerkat/mixer/RerandomizeTest.java index 7f1175c..dea0069 100644 --- a/mixer/src/test/java/meerkat/mixer/RerandomizeTest.java +++ b/mixer/src/test/java/meerkat/mixer/RerandomizeTest.java @@ -46,7 +46,7 @@ public class RerandomizeTest { private ECPoint convert2ECPoint(ByteString bs){ return group.decode(bs.toByteArray()); } - + public void oneRerandomizeTest() throws InvalidProtocolBufferException { Voting.PlaintextBallot msg = Utils.genRandomBallot(2,3,16); // 2 questions with 3 answers each, in range 0-15. @@ -83,7 +83,7 @@ public class RerandomizeTest { int tests = 1000; for (int i = 0; i < tests; i ++){ - System.out.println("rerandomiz test #" + i); + System.out.println("re-randomize test #" + i); oneRerandomizeTest(); } } diff --git a/mixer/src/test/java/meerkat/mixer/Utils.java b/mixer/src/test/java/meerkat/mixer/Utils.java index 38ed3cc..06329ef 100644 --- a/mixer/src/test/java/meerkat/mixer/Utils.java +++ b/mixer/src/test/java/meerkat/mixer/Utils.java @@ -43,7 +43,7 @@ public class Utils { try { KeyFactory fact = KeyFactory.getInstance(ENCRYPTION_KEY_ALGORITHM, - GlobalCryptoSetup.getBouncyCastleProvider()); + GlobalCryptoSetup.getInstance().getBouncyCastleProvider()); PublicKey javaPk = fact.generatePublic(pubKeySpec); ConcreteCrypto.ElGamalPublicKey serializedPk = ConcreteCrypto.ElGamalPublicKey.newBuilder() .setSubjectPublicKeyInfo(ByteString.copyFrom(javaPk.getEncoded())).build(); @@ -79,7 +79,7 @@ public class Utils { try { java.lang.reflect.Method newBuilder = plaintextMessageType.getMethod("newBuilder"); - GeneratedMessage.Builder builder = (GeneratedMessage.Builder) newBuilder.invoke(plaintextMessageType); + Message.Builder builder = (Message.Builder) newBuilder.invoke(plaintextMessageType); builder.mergeDelimitedFrom(in); return plaintextMessageType.cast(builder.build()); } catch (Exception e) { diff --git a/mixer/src/test/java/meerkat/mixer/ZeroKnowledgeProofTest.java b/mixer/src/test/java/meerkat/mixer/ZeroKnowledgeProofTest.java index cb7ea3f..4f2daca 100644 --- a/mixer/src/test/java/meerkat/mixer/ZeroKnowledgeProofTest.java +++ b/mixer/src/test/java/meerkat/mixer/ZeroKnowledgeProofTest.java @@ -10,6 +10,7 @@ import meerkat.mixer.verifier.Verifier; import meerkat.protobuf.ConcreteCrypto; import meerkat.protobuf.Crypto; import meerkat.protobuf.Voting; +//import meerkat.protobuf.Voting.PlaintextBallot; import org.bouncycastle.math.ec.ECPoint; import org.factcenter.qilin.primitives.RandomOracle; import org.factcenter.qilin.primitives.concrete.DigestOracle; @@ -17,6 +18,8 @@ import org.factcenter.qilin.primitives.concrete.ECElGamal; import org.factcenter.qilin.primitives.concrete.ECGroup; import org.junit.Before; import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import java.math.BigInteger; import java.util.Random; @@ -63,10 +66,10 @@ public class ZeroKnowledgeProofTest { Crypto.RerandomizableEncryptedMessage e1New = enc.rerandomize(e1, r1); Crypto.RerandomizableEncryptedMessage e2New = enc.rerandomize(e2, r2); - assert (Utils.decrypt(Voting.PlaintextBallot.class, key, group, e1).equals(msg1)); - assert (Utils.decrypt(Voting.PlaintextBallot.class, key, group, e1New).equals(msg1)); - assert (Utils.decrypt(Voting.PlaintextBallot.class, key, group, e2).equals(msg2)); - assert (Utils.decrypt(Voting.PlaintextBallot.class, key, group, e2New).equals(msg2)); + assertEquals (Utils.decrypt(Voting.PlaintextBallot.class, key, group, e1), msg1); + assertEquals (Utils.decrypt(Voting.PlaintextBallot.class, key, group, e1New), msg1); + assertEquals (Utils.decrypt(Voting.PlaintextBallot.class, key, group, e2), msg2); + assertEquals (Utils.decrypt(Voting.PlaintextBallot.class, key, group, e2New), msg2); ECPoint g = group.getGenerator(); ECPoint h = enc.getElGamalPK().getPK(); @@ -77,16 +80,16 @@ public class ZeroKnowledgeProofTest { ConcreteCrypto.ElGamalCiphertext e2TagElGamal = ECElGamalEncryption.RerandomizableEncryptedMessage2ElGamalCiphertext(e2New); - assert (g.multiply(enc.extractRandomness(r1)).equals( - group.add(convert2ECPoint(e1TagElGamal.getC1()),group.negate(convert2ECPoint(e1ElGamal.getC1()))))); - assert (h.multiply(enc.extractRandomness(r1)).equals( - group.add(convert2ECPoint(e1TagElGamal.getC2()),group.negate(convert2ECPoint(e1ElGamal.getC2()))))); - assert (g.multiply(enc.extractRandomness(r2)).equals( - group.add(convert2ECPoint(e2TagElGamal.getC1()),group.negate(convert2ECPoint(e2ElGamal.getC1()))))); - assert (h.multiply(enc.extractRandomness(r2)).equals( - group.add(convert2ECPoint(e2TagElGamal.getC2()),group.negate(convert2ECPoint(e2ElGamal.getC2()))))); + assertEquals (g.multiply(enc.extractRandomness(r1)), + group.add(convert2ECPoint(e1TagElGamal.getC1()),group.negate(convert2ECPoint(e1ElGamal.getC1())))); + assertEquals (h.multiply(enc.extractRandomness(r1)), + group.add(convert2ECPoint(e1TagElGamal.getC2()),group.negate(convert2ECPoint(e1ElGamal.getC2())))); + assertEquals (g.multiply(enc.extractRandomness(r2)), + group.add(convert2ECPoint(e2TagElGamal.getC1()),group.negate(convert2ECPoint(e2ElGamal.getC1())))); + assertEquals (h.multiply(enc.extractRandomness(r2)), + group.add(convert2ECPoint(e2TagElGamal.getC2()),group.negate(convert2ECPoint(e2ElGamal.getC2())))); - assert (verifier.verify(e1,e2,e1New,e2New,prover.prove(e1,e2,e1New,e2New,false,0,0,0,r1,r2))); + assertTrue (verifier.verify(e1,e2,e1New,e2New,prover.prove(e1,e2,e1New,e2New,false,0,0,0,r1,r2))); } @Test diff --git a/polling-station/build.gradle b/polling-station/build.gradle index 34fe58d..68aad49 100644 --- a/polling-station/build.gradle +++ b/polling-station/build.gradle @@ -41,6 +41,14 @@ version += "${isSnapshot ? '-SNAPSHOT' : ''}" dependencies { // Meerkat common compile project(':meerkat-common') + compile project(':restful-api-common') + + // Jersey for RESTful API + compile 'org.glassfish.jersey.containers:jersey-container-servlet:2.5.+' + + // Servlets + compile 'org.eclipse.jetty:jetty-server:9.3.+' + compile 'org.eclipse.jetty:jetty-servlet:9.3.+' // Logging compile 'org.slf4j:slf4j-api:1.7.7' diff --git a/polling-station/src/main/java/meerkat/pollingstation/PollingStationScannerWebApp.java b/polling-station/src/main/java/meerkat/pollingstation/PollingStationScannerWebApp.java new file mode 100644 index 0000000..327c774 --- /dev/null +++ b/polling-station/src/main/java/meerkat/pollingstation/PollingStationScannerWebApp.java @@ -0,0 +1,88 @@ +package meerkat.pollingstation; + +/** + * Created by Arbel on 5/31/2016. + */ + +import com.google.common.util.concurrent.FutureCallback; +import com.google.protobuf.BoolValue; +import meerkat.protobuf.PollingStation; + +import javax.annotation.PostConstruct; +import javax.servlet.ServletContext; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import java.io.IOException; + +import static meerkat.pollingstation.PollingStationConstants.POLLING_STATION_WEB_SCANNER_ERROR_PATH; +import static meerkat.pollingstation.PollingStationConstants.POLLING_STATION_WEB_SCANNER_SCAN_PATH; +import static meerkat.rest.Constants.MEDIATYPE_PROTOBUF; + +/** + * Implements a Web-App interface for {@link meerkat.pollingstation.PollingStationScanner.Producer} + * This class depends on {@link meerkat.pollingstation.PollingStationWebScanner} and works in conjunction with it + */ +@Path("/") +public class PollingStationScannerWebApp implements PollingStationScanner.Producer { + + @Context + ServletContext servletContext; + + Iterable> callbacks; + + /** + * This method is called by the Jetty engine when instantiating the servlet + */ + @PostConstruct + public void init() throws Exception{ + callbacks = (Iterable>) servletContext.getAttribute(PollingStationWebScanner.CALLBACKS_ATTRIBUTE_NAME); + } + + @POST + @Path(POLLING_STATION_WEB_SCANNER_SCAN_PATH) + @Consumes(MEDIATYPE_PROTOBUF) + @Produces(MEDIATYPE_PROTOBUF) + @Override + public BoolValue newScan(PollingStation.ScannedData scannedData) { + + boolean handled = false; + + for (FutureCallback callback : callbacks){ + + callback.onSuccess(scannedData); + handled = true; + + } + + return BoolValue.newBuilder() + .setValue(handled) + .build(); + + } + + @POST + @Path(POLLING_STATION_WEB_SCANNER_ERROR_PATH) + @Consumes(MEDIATYPE_PROTOBUF) + @Produces(MEDIATYPE_PROTOBUF) + @Override + public BoolValue reportScanError(PollingStation.ErrorMsg errorMsg) { + + boolean handled = false; + + for (FutureCallback callback : callbacks){ + + callback.onFailure(new IOException(errorMsg.getMsg())); + handled = true; + + } + + return BoolValue.newBuilder() + .setValue(handled) + .build(); + + } + +} diff --git a/polling-station/src/main/java/meerkat/pollingstation/PollingStationWebScanner.java b/polling-station/src/main/java/meerkat/pollingstation/PollingStationWebScanner.java new file mode 100644 index 0000000..a4e290a --- /dev/null +++ b/polling-station/src/main/java/meerkat/pollingstation/PollingStationWebScanner.java @@ -0,0 +1,60 @@ +package meerkat.pollingstation; + +import java.util.List; +import java.util.LinkedList; + +import com.google.common.util.concurrent.FutureCallback; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.*; + +import org.glassfish.jersey.servlet.ServletContainer; +import org.glassfish.jersey.server.ResourceConfig; + +import meerkat.protobuf.PollingStation.ScannedData; +import meerkat.rest.*; + +/** + * Created by Arbel on 05/05/2016. + */ + +public class PollingStationWebScanner implements PollingStationScanner.Consumer{ + + public final static String CALLBACKS_ATTRIBUTE_NAME = "callbacks"; + + private final Server server; + private final List> callbacks; + + public PollingStationWebScanner(int port, String subAddress) { + + callbacks = new LinkedList<>(); + + server = new Server(port); + + ServletContextHandler servletContextHandler = new ServletContextHandler(server, subAddress); + servletContextHandler.setAttribute(CALLBACKS_ATTRIBUTE_NAME, (Iterable>) callbacks); + + ResourceConfig resourceConfig = new ResourceConfig(PollingStationScannerWebApp.class); + resourceConfig.register(ProtobufMessageBodyReader.class); + resourceConfig.register(ProtobufMessageBodyWriter.class); + + ServletHolder servletHolder = new ServletHolder(new ServletContainer(resourceConfig)); + + servletContextHandler.addServlet(servletHolder, "/*"); + } + + @Override + public void start() throws Exception { + server.start(); + } + + @Override + public void stop() throws Exception { + server.stop(); + } + + @Override + public void subscribe(FutureCallback scanCallback) { + callbacks.add(scanCallback); + } + +} diff --git a/polling-station/src/test/java/meerkat/pollingstation/PollingStationWebScannerTest.java b/polling-station/src/test/java/meerkat/pollingstation/PollingStationWebScannerTest.java new file mode 100644 index 0000000..6e88baa --- /dev/null +++ b/polling-station/src/test/java/meerkat/pollingstation/PollingStationWebScannerTest.java @@ -0,0 +1,162 @@ +package meerkat.pollingstation; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.protobuf.ByteString; +import meerkat.protobuf.PollingStation.*; +import meerkat.rest.Constants; + +import meerkat.rest.*; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import java.util.concurrent.Semaphore; + +import static meerkat.pollingstation.PollingStationConstants.*; + +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Created by Arbel on 25/05/2016. + */ +public class PollingStationWebScannerTest { + + private PollingStationScanner.Consumer scanner; + private static final String ADDRESS = "http://localhost"; + private static final String SUB_ADDRESS = ""; + private static final int PORT = 8080; + + private Semaphore semaphore; + private Throwable thrown; + private boolean dataIsAsExpected; + + private class ScanHandler implements FutureCallback { + + private final ScannedData expectedData; + + public ScanHandler(ScannedData expectedData) { + this.expectedData = expectedData; + } + + @Override + public void onSuccess(ScannedData result) { + dataIsAsExpected = result.getChannel().equals(expectedData.getChannel()); + semaphore.release(); + } + + @Override + public void onFailure(Throwable t) { + dataIsAsExpected = false; + thrown = t; + semaphore.release(); + } + } + + private class ErrorHandler implements FutureCallback { + + private final String expectedErrorMessage; + + public ErrorHandler(String expectedErrorMessage) { + this.expectedErrorMessage = expectedErrorMessage; + } + + @Override + public void onSuccess(ScannedData result) { + dataIsAsExpected = false; + semaphore.release(); + } + + @Override + public void onFailure(Throwable t) { + dataIsAsExpected = t.getMessage().equals(expectedErrorMessage); + semaphore.release(); + } + } + + @Before + public void init() { + + System.err.println("Setting up Scanner WebApp!"); + + scanner = new PollingStationWebScanner(PORT, SUB_ADDRESS); + + semaphore = new Semaphore(0); + thrown = null; + + try { + scanner.start(); + } catch (Exception e) { + assertThat("Could not start server: " + e.getMessage(), false); + } + + } + + @Test + public void testSuccessfulScan() throws InterruptedException { + + Client client = ClientBuilder.newClient(); + client.register(ProtobufMessageBodyReader.class); + client.register(ProtobufMessageBodyWriter.class); + WebTarget webTarget = client.target(ADDRESS + ":" + PORT) + .path(SUB_ADDRESS).path(POLLING_STATION_WEB_SCANNER_SCAN_PATH); + + byte[] data = {(byte) 1, (byte) 2}; + + ScannedData scannedData = ScannedData.newBuilder() + .setChannel(ByteString.copyFrom(data)) + .build(); + + scanner.subscribe(new ScanHandler(scannedData)); + + Response response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(scannedData, Constants.MEDIATYPE_PROTOBUF)); + response.close(); + + semaphore.acquire(); + assertThat("Scanner has thrown an error", thrown == null); + assertThat("Scanned data received was incorrect", dataIsAsExpected); + + } + + @Test + public void testErroneousScan() throws InterruptedException { + + Client client = ClientBuilder.newClient(); + client.register(ProtobufMessageBodyReader.class); + client.register(ProtobufMessageBodyWriter.class); + WebTarget webTarget = client.target(ADDRESS + ":" + PORT) + .path(SUB_ADDRESS).path(POLLING_STATION_WEB_SCANNER_ERROR_PATH); + + ErrorMsg errorMsg = ErrorMsg.newBuilder() + .setMsg("!Error Message!") + .build(); + + scanner.subscribe(new ErrorHandler(errorMsg.getMsg())); + + Response response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(errorMsg, Constants.MEDIATYPE_PROTOBUF)); + response.close(); + + semaphore.acquire(); + assertThat("Scanner error received was incorrect", dataIsAsExpected); + + } + + @After + public void close() { + + System.err.println("Scanner WebApp shutting down..."); + + try { + scanner.stop(); + } catch (Exception e) { + assertThat("Could not stop server: " + e.getMessage(), false); + } + + } + +} \ No newline at end of file diff --git a/restful-api-common/src/main/java/meerkat/rest/Constants.java b/restful-api-common/src/main/java/meerkat/rest/Constants.java index 73ed7d1..2c04248 100644 --- a/restful-api-common/src/main/java/meerkat/rest/Constants.java +++ b/restful-api-common/src/main/java/meerkat/rest/Constants.java @@ -5,8 +5,4 @@ package meerkat.rest; */ public interface Constants { public static final String MEDIATYPE_PROTOBUF = "application/x-protobuf"; - - public static final String BULLETIN_BOARD_SERVER_PATH = "/bbserver"; - public static final String READ_MESSAGES_PATH = "/readmessages"; - public static final String POST_MESSAGE_PATH = "/postmessage"; } diff --git a/restful-api-common/src/main/java/meerkat/rest/ProtobufMessageBodyReader.java b/restful-api-common/src/main/java/meerkat/rest/ProtobufMessageBodyReader.java index d09e213..ebf80a0 100644 --- a/restful-api-common/src/main/java/meerkat/rest/ProtobufMessageBodyReader.java +++ b/restful-api-common/src/main/java/meerkat/rest/ProtobufMessageBodyReader.java @@ -32,7 +32,7 @@ public class ProtobufMessageBodyReader implements MessageBodyReader { InputStream entityStream) throws IOException, WebApplicationException { try { Method newBuilder = type.getMethod("newBuilder"); - GeneratedMessage.Builder builder = (GeneratedMessage.Builder) newBuilder.invoke(type); + Message.Builder builder = (Message.Builder) newBuilder.invoke(type); return builder.mergeFrom(entityStream).build(); } catch (Exception e) { throw new WebApplicationException(e); diff --git a/settings.gradle b/settings.gradle index 3de9b3e..542a831 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,3 +5,4 @@ include 'polling-station' include 'restful-api-common' include 'mixer' include 'bulletin-board-client' +include 'distributed-key-generation' diff --git a/voting-booth/build.gradle b/voting-booth/build.gradle index 70d7340..544f0b4 100644 --- a/voting-booth/build.gradle +++ b/voting-booth/build.gradle @@ -1,4 +1,3 @@ - plugins { id "us.kirchmeier.capsule" version "1.0.1" id 'com.google.protobuf' version '0.7.0' @@ -40,6 +39,15 @@ version += "${isSnapshot ? '-SNAPSHOT' : ''}" dependencies { // Meerkat common compile project(':meerkat-common') + compile project(':restful-api-common') + + // Jersey for RESTful API + compile 'org.glassfish.jersey.containers:jersey-container-servlet:2.5.+' + + // Servlets + compile group: 'org.eclipse.jetty', name: 'jetty-server', version: '9.3.+' + compile 'org.eclipse.jetty:jetty-server:9.3.+' + compile 'org.eclipse.jetty:jetty-servlet:9.3.+' // Logging compile 'org.slf4j:slf4j-api:1.7.7' @@ -52,6 +60,10 @@ dependencies { testCompile 'junit:junit:4.+' runtime 'org.codehaus.groovy:groovy:2.4.+' + + // Meerkat polling station + compile project(':polling-station') + } @@ -188,6 +200,3 @@ publishing { } } } - - - diff --git a/voting-booth/src/main/java/meerkat/voting/ToyEncryption.java b/voting-booth/src/main/java/meerkat/voting/ToyEncryption.java new file mode 100644 index 0000000..e0089c8 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ToyEncryption.java @@ -0,0 +1,46 @@ +package meerkat.voting; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import meerkat.crypto.Encryption; +import meerkat.protobuf.Crypto.*; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Random; + +/** + * Created by hai on 07/06/16. + */ +public class ToyEncryption implements Encryption { + + @Override + public RerandomizableEncryptedMessage encrypt(Message plaintext, EncryptionRandomness rnd) throws IOException { + ByteString cipher = ByteString.copyFromUtf8("Encryption(") + .concat(plaintext.toByteString()) + .concat(ByteString.copyFromUtf8(", Random(")) + .concat(rnd.getData()) + .concat(ByteString.copyFromUtf8("))")); + return RerandomizableEncryptedMessage.newBuilder() + .setData(cipher) + .build(); + } + + @Override + public RerandomizableEncryptedMessage rerandomize + (RerandomizableEncryptedMessage msg, EncryptionRandomness rnd) throws InvalidProtocolBufferException { + throw new UnsupportedOperationException(); + } + + @Override + public EncryptionRandomness generateRandomness(Random rand) { + ByteBuffer b = ByteBuffer.allocate(4); + b.putInt(rand.nextInt()); + byte[] bArr = b.array(); + ByteString bs = ByteString.copyFrom(bArr); + return EncryptionRandomness.newBuilder() + .setData(bs) + .build(); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ToySignature.java b/voting-booth/src/main/java/meerkat/voting/ToySignature.java new file mode 100644 index 0000000..89bfc58 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ToySignature.java @@ -0,0 +1,90 @@ +package meerkat.voting; + +import com.google.protobuf.ByteString; +import com.google.protobuf.Message; +import meerkat.crypto.DigitalSignature; +import meerkat.protobuf.Crypto.*; +import meerkat.protobuf.Crypto.Signature; + +import java.io.IOException; +import java.io.InputStream; +import java.security.*; +import java.security.cert.CertificateException; + +/** + * Created by hai on 07/06/16. + */ +public class ToySignature implements DigitalSignature { + + private final ByteString signerID; + private ByteString msgByteString; + + + public ToySignature(String signerID) { + this.signerID = ByteString.copyFromUtf8(signerID); + } + + @Override + public ByteString getSignerID() { + return signerID; + } + + @Override + public void updateContent(Message msg) throws SignatureException { + msgByteString = msg.toByteString(); + } + + @Override + public Signature sign() throws SignatureException { + ByteString signature = ByteString.copyFromUtf8("Signature(") + .concat(msgByteString) + .concat(ByteString.copyFromUtf8(")")); + return Signature.newBuilder() + .setType(SignatureType.ECDSA) + .setData(signature) + .setSignerId(getSignerID()) + .build(); + } + + + @Override + public void loadVerificationCertificates(InputStream certStream) throws CertificateException { + throw new UnsupportedOperationException(); + } + + @Override + public void clearVerificationCertificates() { + throw new UnsupportedOperationException(); + } + + @Override + public void updateContent(byte[] data) throws SignatureException { + throw new UnsupportedOperationException(); + } + + @Override + public void initVerify(Signature sig) throws CertificateException, InvalidKeyException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean verify() { + throw new UnsupportedOperationException(); + } + + @Override + public KeyStore.Builder getPKCS12KeyStoreBuilder(InputStream keyStream, char[] password) throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException { + throw new UnsupportedOperationException(); + } + + @Override + public void loadSigningCertificate(KeyStore.Builder keyStoreBuilder) throws IOException, CertificateException, UnrecoverableKeyException { + throw new UnsupportedOperationException(); + } + + + @Override + public void clearSigningKey() { + throw new UnsupportedOperationException(); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/VotingBoothToyRun.java b/voting-booth/src/main/java/meerkat/voting/VotingBoothToyRun.java new file mode 100644 index 0000000..65b6685 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/VotingBoothToyRun.java @@ -0,0 +1,290 @@ +package meerkat.voting; + +import com.google.protobuf.ByteString; +import meerkat.crypto.DigitalSignature; +import meerkat.crypto.Encryption; +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.*; +import meerkat.voting.output.*; +import meerkat.voting.storage.*; +import meerkat.voting.encryptor.*; +import meerkat.voting.ui.*; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.*; + +/** + * Created by hai on 26/04/16. + */ +public class VotingBoothToyRun { + + public static void main(String[] args) { + + try { + generateSystemMessages(); + generateDemoQuestions(); + } + catch (Exception e) { + return; + } + + Random rand = new Random(); + Encryption enc = new ToyEncryption(); + DigitalSignature sig = new ToySignature("MY_SIGNER_ID"); + + StorageManager storageManager = new StorageManagerMockup(); + SystemConsoleOutputDevice outputDevice = new SystemConsoleOutputDevice(); + VBCryptoManager cryptoManager = new VBCryptoManagerImpl(rand, enc, sig); + SystemConsoleUI ui = new SystemConsoleUI (); + + + VotingBoothImpl controller = new VotingBoothImpl(); + + try { + controller.init(outputDevice, cryptoManager, ui, storageManager); + } + catch (Exception e) { + System.err.println("init failed"); + return; + } + + + // create threads + + + Thread controllerThread = new Thread(controller); + controllerThread.setName("Meerkat VB-Controller Thread"); + Thread uiThread = new Thread(ui); + uiThread.setName("Meerkat VB-UI Thread"); + Thread outputThread = new Thread(outputDevice); + outputThread.setName("Meerkat VB-Output Thread"); + + uiThread.start(); + controllerThread.start(); + outputThread.start(); + + } + + + private static void generateDemoQuestions() throws IOException { + + ElectionParams electionParams = ElectionParams.newBuilder() + .addAllRaceQuestions(generateBallotQuestions()) + .addAllChannelChoiceQuestions(generateChannelChoiceQuestions()) + .setSelectionData(generateSelectionData()) + .build(); + + try { + FileOutputStream output = new FileOutputStream(StorageManagerMockup.electionParamFullFilename); + electionParams.writeTo(output); + output.close(); + System.out.println("Successfully wrote election parameter protobuf to a file"); + } + catch (IOException e) { + System.err.println("Could not write to the election parameter file: '" + StorageManagerMockup.electionParamFullFilename + "'."); + throw e; + } + + } + + + private static List generateChannelChoiceQuestions() { + ArrayList channelChoiceQuestions = new ArrayList(); + + String[] ans1 = {"Red", "Blue", "Green"}; + BallotQuestion ccquestion1 = generateBallotQuestion("What is your favorite color?", "Pick one answer", ans1); + channelChoiceQuestions.add(ccquestion1); + + String[] ans2 = {"Yes", "No"}; + BallotQuestion ccquestion2 = generateBallotQuestion("Are you a republican?", "Pick one answer", ans2); + channelChoiceQuestions.add(ccquestion2); + + return channelChoiceQuestions; + } + + + private static List generateBallotQuestions() { + ArrayList allBallotQuestions = new ArrayList(); + + String[] answers1 = {"answer 1", "answer 2", "answer 3", "answer 4"}; + allBallotQuestions.add(generateBallotQuestion("question 1. Asking something...", "Pick one answer", answers1)); + + String[] answers2 = {"Miranda Kerr", "Doutzen Kroes", "Moran Atias", "Roslana Rodina", "Adriana Lima"}; + allBallotQuestions.add(generateBallotQuestion("question 2: Which model do you like", "Mark as many as you want", answers2)); + + allBallotQuestions.add(generateBallotQuestion("question 3. Asking something...", "Pick one answer", answers1)); + allBallotQuestions.add(generateBallotQuestion("question 4. Asking something...", "Pick one answer", answers1)); + + String[] answers5 = {"Clint Eastwood", "Ninja", "Sonic", "Tai-chi", "Diablo", "Keanu"}; + allBallotQuestions.add(generateBallotQuestion("question 5: Good name for a cat", "Pick the best one", answers5)); + + allBallotQuestions.add(generateBallotQuestion("question 6. Asking something...", "Pick one answer", answers1)); + allBallotQuestions.add(generateBallotQuestion("question 7. Asking something...", "Pick one answer", answers1)); + allBallotQuestions.add(generateBallotQuestion("question 8. Asking something...", "Pick one answer", answers1)); + allBallotQuestions.add(generateBallotQuestion("question 9. Asking something...", "Pick one answer", answers1)); + allBallotQuestions.add(generateBallotQuestion("question 10. Asking something...", "Pick one answer", answers1)); + allBallotQuestions.add(generateBallotQuestion("question 11. Asking something...", "Pick one answer", answers1)); + allBallotQuestions.add(generateBallotQuestion("question 12. Asking something...", "Pick one answer", answers1)); + allBallotQuestions.add(generateBallotQuestion("question 13. Asking something...", "Pick one answer", answers1)); + allBallotQuestions.add(generateBallotQuestion("question 14. Asking something...", "Pick one answer", answers1)); + + return allBallotQuestions; + } + + + private static BallotQuestion generateBallotQuestion(String questionStr, String descriptionStr, String[] answers) { + UIElement question = UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(stringToBytes(questionStr)) + .build(); + + UIElement description = UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(stringToBytes(descriptionStr)) + .build(); + + BallotQuestion.Builder bqb = BallotQuestion.newBuilder(); + bqb.setIsMandatory(false); + bqb.setQuestion(question); + bqb.setDescription(description); + for (String answerStr : answers) { + UIElement answer = UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(stringToBytes(answerStr)) + .build(); + bqb.addAnswer(answer); + } + + return bqb.build(); + } + + + private static SimpleCategoriesSelectionData generateSelectionData() { + Category sharedDefaults = Category.newBuilder() + .addQuestionIndex(0) + .addQuestionIndex(5) + .addQuestionIndex(9) + .build(); + + Category cat00 = Category.newBuilder() + .addQuestionIndex(1) + .addQuestionIndex(4) + .addQuestionIndex(6) + .addQuestionIndex(7) + .build(); + + Category cat01 = Category.newBuilder() + .addQuestionIndex(2) + .addQuestionIndex(4) + .addQuestionIndex(8) + .build(); + + Category cat02 = Category.newBuilder() + .addQuestionIndex(3) + .addQuestionIndex(8) + .build(); + + Category cat10 = Category.newBuilder() + .addQuestionIndex(10) + .addQuestionIndex(11) + .build(); + + Category cat11 = Category.newBuilder() + .addQuestionIndex(12) + .addQuestionIndex(13) + .build(); + + CategoryChooser catChooser0 = CategoryChooser.newBuilder() + .addCategory(cat00) + .addCategory(cat01) + .addCategory(cat02) + .build(); + + CategoryChooser catChooser1 = CategoryChooser.newBuilder() + .addCategory(cat10) + .addCategory(cat11) + .build(); + + return SimpleCategoriesSelectionData.newBuilder() + .setSharedDefaults(sharedDefaults) + .addCategoryChooser(catChooser0) + .addCategoryChooser(catChooser1) + .build(); + + } + + + + private static ByteString stringToBytes (String s) { + return ByteString.copyFromUtf8(s); + } + + + private static void generateSystemMessages() throws IOException{ + Map systemMessageMap = new HashMap(); + + systemMessageMap.put(StorageManager.WAIT_FOR_COMMIT_MESSAGE, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Please wait while committing to ballot")) + .build()); + systemMessageMap.put(StorageManager.WAIT_FOR_AUDIT_MESSAGE, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Please wait while auditing your ballot")) + .build()); + systemMessageMap.put(StorageManager.WAIT_FOR_CAST_MESSAGE, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Please wait while finalizing your ballot for voting")) + .build()); + systemMessageMap.put(StorageManager.RESTART_VOTING_BUTTON, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Restart voting")) + .build()); + systemMessageMap.put(StorageManager.UNRECOGNIZED_FINALIZE_RESPONSE_MESSAGE, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Could not understand response for Cast or Audit. Force restarting.")) + .build()); + systemMessageMap.put(StorageManager.UNSUCCESSFUL_CHANNEL_CHOICE_MESSAGE, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Choice of channel was unsuccessful. Force restarting.")) + .build()); + systemMessageMap.put(StorageManager.OUTPUT_DEVICE_FAILURE_MESSAGE, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Ballot output device failure. Force restarting.")) + .build()); + systemMessageMap.put(StorageManager.UNSUCCESSFUL_VOTING_MESSAGE, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Voting was unsuccessful. Force restarting.")) + .build()); + systemMessageMap.put(StorageManager.SOMETHING_WRONG_MESSAGE, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Something was terribly wrong. Force restarting.")) + .build()); + systemMessageMap.put(StorageManager.ENCRYPTION_FAILED_MESSAGE, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Encryption failed for some unknown reason.")) + .build()); + systemMessageMap.put(StorageManager.RETRY_BUTTON, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Retry")) + .build()); + systemMessageMap.put(StorageManager.CANCEL_VOTE_BUTTON, UIElement.newBuilder() + .setType(UIElementDataType.TEXT) + .setData(ByteString.copyFromUtf8("Cancel Vote")) + .build()); + + BoothSystemMessages systemMessages = BoothSystemMessages.newBuilder().putAllSystemMessage(systemMessageMap).build(); + + try { + FileOutputStream output = new FileOutputStream(StorageManagerMockup.systemMessagesFilename); + systemMessages.writeTo(output); + output.close(); + System.out.println("Successfully wrote system messages protobuf to a file"); + } + catch (IOException e) { + System.err.println("Could not write to the system messages file: '" + StorageManagerMockup.systemMessagesFilename + "'."); + throw e; + } + + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/VotingBoothController.java b/voting-booth/src/main/java/meerkat/voting/controller/VotingBoothController.java new file mode 100644 index 0000000..6008d24 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/VotingBoothController.java @@ -0,0 +1,34 @@ +package meerkat.voting.controller; + +import meerkat.voting.encryptor.VBCryptoManager; +import meerkat.voting.ui.VotingBoothUI; +import meerkat.voting.output.BallotOutputDevice; +import meerkat.voting.storage.StorageManager; + +import java.io.IOException; + + +/** + * An interface for the controller component of the voting booth + */ +public interface VotingBoothController extends Runnable { + + /** + * initialize by setting all the different components of the Voting Booth to be recognized by this controller + * @param outputDevice the ballot output device. Naturally a printer and/or ethernet connection + * @param vbCrypto the crypto module + * @param vbUI User interface in which the voter chooses his answers + * @param vbStorageManager storage component for handling files and USB sticks + */ + public void init (BallotOutputDevice outputDevice, + VBCryptoManager vbCrypto, + VotingBoothUI vbUI, + StorageManager vbStorageManager) throws IOException; + + /** + * an asynchronous call from Admin Console (If there is such one implemented) to shut down the system + */ + public void callShutDown(); + + +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/VotingBoothImpl.java b/voting-booth/src/main/java/meerkat/voting/controller/VotingBoothImpl.java new file mode 100644 index 0000000..4903424 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/VotingBoothImpl.java @@ -0,0 +1,473 @@ +package meerkat.voting.controller; + +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.callbacks.*; +import meerkat.voting.controller.commands.*; +import meerkat.voting.controller.selector.QuestionSelector; +import meerkat.voting.controller.selector.SimpleListCategoriesSelector; +import meerkat.voting.encryptor.VBCryptoManager; +import meerkat.voting.encryptor.VBCryptoManager.EncryptionAndSecrets; +import meerkat.voting.output.BallotOutputDevice; +import meerkat.voting.storage.StorageManager; +import meerkat.voting.ui.VotingBoothUI; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.security.SignatureException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * An asynchronous implementation of the VotingBoothController. + * This implementation binds the other components (storage, ui, output device, and crypto manager), + * and runs as its own thread controlling the whole VB process. + * The high level details are that it has a queue of commands to handle in order, and a State object which keeps + * all data from previous tasks which is necessary for the next task. + * It calls executions in the UI and output device asynchronously. + */ +public class VotingBoothImpl implements VotingBoothController { + + private final Logger logger = LoggerFactory.getLogger(VotingBoothImpl.class); + + // the component interfaces of the Voting Booth + private BallotOutputDevice outputDevice; + private VBCryptoManager crypto; + private VotingBoothUI ui; + private StorageManager storageManager; + + // election details and info + private List questionsForChoosingChannel; + private QuestionSelector questionSelector; + private Map systemMessages; + + // state + private ControllerState state; + private volatile boolean shutDownHasBeenCalled; + private LinkedBlockingQueue queue; + protected final int MAX_REQUEST_IDENTIFIER = 100000; + private static int requestCounter = 0; + + + // a simple constructor + public VotingBoothImpl () { + logger.info("A VotingBoothImpl is constructed"); + shutDownHasBeenCalled = false; + queue = new LinkedBlockingQueue<>(); + state = new ControllerState(); + } + + @Override + public void init(BallotOutputDevice outputDevice, + VBCryptoManager vbCrypto, + VotingBoothUI vbUI, + StorageManager vbStorageManager) throws IOException { + logger.info("init is called"); + + // keep pointers to the VB components + this.outputDevice = outputDevice; + this.crypto = vbCrypto; + this.ui = vbUI; + this.storageManager = vbStorageManager; + + // store election details and info + ElectionParams electionParams; + try { + logger.info("init: reading election params"); + electionParams = storageManager.readElectionParams(); + logger.info("init: reading system messages"); + systemMessages = storageManager.readSystemMessages(); + } + catch (IOException e) { + logger.error("init could not read info from a file. Exception is: " + e); + throw e; + } + + logger.info("init: setting the election parameters"); + this.questionsForChoosingChannel = electionParams.getChannelChoiceQuestionsList(); + List questions = electionParams.getRaceQuestionsList(); + this.questionSelector = new SimpleListCategoriesSelector(questions, electionParams.getSelectionData()); + logger.info("init: setting finished"); + } + + + @Override + public void run() { + logger.info("run command has been called"); + runVotingFlow(); + doShutDown(); + + } + + /** + * a method for running the Voting flow of the VB (in contrast to the admin-setup flow + * It simply loops: takes the next command in its inner queue and handles it + */ + private void runVotingFlow () { + logger.info("entered the voting flow"); + + queue.add(new RestartVotingCommand(generateRequestIdentifier(), state.currentBallotSerialNumber)); + + while (! wasShutDownCalled()) { + try { + ControllerCommand Command = queue.take(); + handleSingleCommand(Command); + } + catch (InterruptedException e) { + logger.warn ("Interrupted while reading from command queue " + e); + } + } + } + + @Override + public void callShutDown() { + logger.info("callShutDown command has been called"); + shutDownHasBeenCalled = true; + queue.clear(); + ui.callShutDown(); + outputDevice.callShutDown(); + } + + /** + * this method decides upon a given command if to ignore it (if it has an old serial number) or to handle it + * If we choose to handle it, then it simply calls the matching method which handles this type of command + * @param command a command to handle next (probably from the inner command queue) + */ + private void handleSingleCommand(ControllerCommand command) { + // check if the command is old and should be ignored + if (command.getBallotSerialNumber() != state.currentBallotSerialNumber && !(command instanceof RestartVotingCommand)) { + // probably an old command relating to some old ballot serial number. Simply log it and ignore it. + String errorMessage = "handleSingleCommand: received a task too old. " + + command.getBallotSerialNumber() + " " + state.currentBallotSerialNumber; + logger.debug(errorMessage); + return; + } + + // decide which method to run according to the command type + if (command instanceof RestartVotingCommand) { + doRestartVoting (); + } + else if (command instanceof ChannelChoiceCommand) { + doChooseChannel(); + } + else if (command instanceof ChannelDeterminedCommand) { + doSetChannelAndAskQuestions ((ChannelDeterminedCommand)command); + } + else if (command instanceof ChooseFinalizeOptionCommand) { + doChooseFinalizeOption(); + } + else if (command instanceof CastCommand) { + doFinalize(false); + } + else if (command instanceof AuditCommand) { + doFinalize(true); + } + else if (command instanceof EncryptAndCommitBallotCommand) { + doCommit ((EncryptAndCommitBallotCommand)command); + } + else if (command instanceof ReportErrorCommand) { + doReportErrorAndForceRestart((ReportErrorCommand)command); + } + else { + logger.error("handleSingleCommand: unknown type of ControllerCommand received: " + command.getClass().getName()); + doReportErrorAndForceRestart(systemMessages.get(StorageManager.SOMETHING_WRONG_MESSAGE)); + } + } + + private boolean wasShutDownCalled () { + return shutDownHasBeenCalled; + } + + private void doShutDown () { + logger.info("running callShutDown"); + state.clearAndResetState(VBState.SHUT_DOWN); + //TODO: add commands to actually shut down the machine + } + + /** + * a method to execute a Restart Voting Command + */ + private void doRestartVoting () { + queue.clear(); + state.clearAndResetState(VBState.NEW_VOTER); + ui.startNewVoterSession(new NewVoterCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue)); + } + + /** + * a (overloaded) method to execute a Report Error Command. + * It actually just runs the overloaded version of this method with the error message inside the command + * @param command the command has the info of the error message to report + */ + private void doReportErrorAndForceRestart(ReportErrorCommand command) { + doReportErrorAndForceRestart(command.getErrorMessage()); + } + + /** + * a (overloaded) method to report an error message to the voter + * @param errorMessage message to show the voter + */ + private void doReportErrorAndForceRestart(UIElement errorMessage) { + queue.clear(); + state.clearAndResetState(VBState.FATAL_ERROR_FORCE_NEW_VOTER); + ui.showErrorMessageWithButtons(errorMessage, + new UIElement[]{systemMessages.get(StorageManager.RESTART_VOTING_BUTTON)}, + new ErrorMessageRestartCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue)); + } + + /** + * a method to execute a Channel Choice Command + * it notifies the UI to present the channel choice questions to the voter + */ + private void doChooseChannel () { + if (state.stateIdentifier == VBState.NEW_VOTER) { + logger.debug("doing chooseChannel"); + state.stateIdentifier = VBState.CHOOSE_CHANNEL; + ui.chooseChannel(this.questionsForChoosingChannel, + new ChannelChoiceCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue, + systemMessages.get(StorageManager.UNSUCCESSFUL_CHANNEL_CHOICE_MESSAGE))); + } + else { + logger.debug("doChooseChannel: current state is " + state.stateIdentifier); + // ignore this request + } + } + + /** + * a method to execute a Channel Determined Command + * (this actually sets the channel now after the voter has answered the channel choice questions) + * It then determines the race questions for the voter, and notifies the UI to present them to the voter + * @param command details of the voter's answers on the channel choice questions + */ + private void doSetChannelAndAskQuestions (ChannelDeterminedCommand command) { + if (state.stateIdentifier == VBState.CHOOSE_CHANNEL) { + logger.debug("doing set channel and ask questions"); + state.stateIdentifier = VBState.ANSWER_QUESTIONS; + List channelChoiceAnswers = command.channelChoiceAnswers; + state.channelIdentifier = questionSelector.getChannelIdentifier(channelChoiceAnswers); + state.channelSpecificQuestions = questionSelector.selectQuestionsForVoter(state.channelIdentifier); + ui.askVoterQuestions(state.channelSpecificQuestions, + new VotingCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue, + systemMessages.get(StorageManager.UNSUCCESSFUL_VOTING_MESSAGE))); + } + else { + logger.debug("doSetChannelAndAskQuestions: current state is " + state.stateIdentifier); + // ignore this request + } + } + + /** + * a method to execute a Do-Finalzie-Option Command + * notifies the UI to present the cast-or-audit question to the voter + */ + private void doChooseFinalizeOption() { + if (state.stateIdentifier == VBState.COMMITTING_TO_BALLOT) { + logger.debug("doChooseFinalizeOption"); + state.stateIdentifier = VBState.CAST_OR_AUDIT; + ui.castOrAudit(new CastOrAuditCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue, + systemMessages.get(StorageManager.UNRECOGNIZED_FINALIZE_RESPONSE_MESSAGE))); + } + else { + logger.debug("doChooseFinalizeOption: current state is " + state.stateIdentifier); + // ignore this request + } + } + + /** + * a method to execute a Encrypt-and-Commit Command + * It sends a notification to commit to the output device + * @param command details of the voter's answers on the ballot questions + */ + private void doCommit (EncryptAndCommitBallotCommand command) { + if (state.stateIdentifier == VBState.ANSWER_QUESTIONS) { + logger.debug("doing commit"); + try { + setBallotData(command); + ui.notifyVoterToWaitForFinish(systemMessages.get(StorageManager.WAIT_FOR_COMMIT_MESSAGE), + new WaitForFinishCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue, + systemMessages.get(StorageManager.SOMETHING_WRONG_MESSAGE))); + outputDevice.commitToBallot(state.plaintextBallot, + state.signedEncryptedBallot, + new OutputDeviceCommitCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue, + systemMessages.get(StorageManager.OUTPUT_DEVICE_FAILURE_MESSAGE))); + state.stateIdentifier = VBState.COMMITTING_TO_BALLOT; + } + catch (SignatureException | IOException e) { + logger.error("doCommit: encryption failed. exception: " + e); + + // in case the encryption failed for some unknown reason, we send the UI a notification + // to ask the voter whether he wants to retry or cancel the ballot + UIElement errorMessage = systemMessages.get(StorageManager.ENCRYPTION_FAILED_MESSAGE); + UIElement[] buttons = new UIElement[]{ + systemMessages.get(StorageManager.RETRY_BUTTON), + systemMessages.get(StorageManager.CANCEL_VOTE_BUTTON)}; + + EncryptionFailedCallback callback = new EncryptionFailedCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue); + ui.showErrorMessageWithButtons(errorMessage, buttons, callback); + } + } + else { + logger.debug("doCommit: current state is " + state.stateIdentifier); + // ignore this request + } + } + + /** + * encrypt the ballot, and keep all info (plaintext, encryption and secrets) in the state's attributes + * @param command either an EncryptAndCommitBallotCommand if we encrypt the plaintext for the first time, or a RetryEncryptAndCommitBallotCommand if we already got here but encryption failed before + * @throws IOException problems in the encryption process + * @throws SignatureException problems in the digital signature process + */ + private void setBallotData (EncryptAndCommitBallotCommand command) throws IOException, SignatureException{ + // a Retry command is given only if we first got here, and later the encryption failed but the voter chose to retry + // in such a case the plaintext is already set from previous attempt + if (! (command instanceof RetryEncryptAndCommitBallotCommand)) { + // this is not a retry attempt, so the plaintext is not set yet + // otherwise, we have the plaintext from the previous encryption attempt + state.plaintextBallot = PlaintextBallot.newBuilder() + .setSerialNumber(command.getBallotSerialNumber()) + .addAllAnswers(command.getVotingAnswers()) + .build(); + } + + // keep the encryption and the secrets we used for it + EncryptionAndSecrets encryptionAndSecrets = crypto.encrypt(state.plaintextBallot); + state.signedEncryptedBallot = encryptionAndSecrets.getSignedEncryptedBallot(); + state.secrets = encryptionAndSecrets.getSecrets(); + } + + /** + * a method to execute a Cast Command or an Audit Command + * according to the flag, chooses which finalize ballot task to send to the output device + * @param auditRequested true if we wish to finalize by auditing. false if we finalize by casting the ballot + */ + private void doFinalize (boolean auditRequested) { + if (state.stateIdentifier == VBState.CAST_OR_AUDIT) { + logger.debug("finalizing"); + state.stateIdentifier = VBState.FINALIZING; + if (auditRequested) { + // finalize by auditing + ui.notifyVoterToWaitForFinish(systemMessages.get(StorageManager.WAIT_FOR_AUDIT_MESSAGE), + new WaitForFinishCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue, + systemMessages.get(StorageManager.SOMETHING_WRONG_MESSAGE))); + outputDevice.audit(state.secrets, + new OutputDeviceFinalizeCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue, + systemMessages.get(StorageManager.OUTPUT_DEVICE_FAILURE_MESSAGE))); + } + else { + // finalize by casting the ballot + ui.notifyVoterToWaitForFinish(systemMessages.get(StorageManager.WAIT_FOR_CAST_MESSAGE), + new WaitForFinishCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue, + systemMessages.get(StorageManager.SOMETHING_WRONG_MESSAGE))); + outputDevice.castBallot( + new OutputDeviceFinalizeCallback(generateRequestIdentifier(), + state.currentBallotSerialNumber, + this.queue, + systemMessages.get(StorageManager.OUTPUT_DEVICE_FAILURE_MESSAGE))); + } + } + else { + logger.debug("doFinalize: current state is " + state.stateIdentifier); + // ignore this request + } + } + + + + // an enum to keep the step (of the voting process) in which the VB is currently in + private enum VBState { + NEW_VOTER, + CHOOSE_CHANNEL, + ANSWER_QUESTIONS, + COMMITTING_TO_BALLOT, + CAST_OR_AUDIT, + FINALIZING, + FATAL_ERROR_FORCE_NEW_VOTER, + SHUT_DOWN + } + + + /** + * a class to keep and directly access all the details of the VB controller state. + * naming: + * - the (enum) state identifier of the current step + * - the chosen channel + * - all details of the ballot (both plaintext and encryption) + * - last request identifier (to one of the other component interfaces) + * - serial number of the current ballot + */ + private class ControllerState { + public VBState stateIdentifier; + public byte[] channelIdentifier; + public List channelSpecificQuestions; + public PlaintextBallot plaintextBallot; + public SignedEncryptedBallot signedEncryptedBallot; + public BallotSecrets secrets; + public int lastRequestIdentifier; + public long currentBallotSerialNumber; + + public ControllerState () { + plaintextBallot = null; + signedEncryptedBallot = null; + secrets = null; + lastRequestIdentifier = -1; + channelIdentifier = null; + channelSpecificQuestions = null; + currentBallotSerialNumber = 0; + } + + private void clearPlaintext () { + plaintextBallot = null; + } + + private void clearCiphertext () { + signedEncryptedBallot = null; + secrets = null; + } + + public void clearAndResetState(VBState newStateIdentifier) { + state.clearPlaintext(); + state.clearCiphertext(); + state.stateIdentifier = newStateIdentifier; + state.currentBallotSerialNumber += 1; + } + + } + + + /** + * Creates a new request identifier to identify any call to one of the other component interfaces. + * We limit its value to MAX_REQUEST_IDENTIFIER, so the identifier is kept short. + * @return a new request identifier + */ + private int generateRequestIdentifier() { + ++requestCounter; + if (requestCounter >= MAX_REQUEST_IDENTIFIER) { + requestCounter = 1; + } + return requestCounter; + } + + +} + diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/CastOrAuditCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/CastOrAuditCallback.java new file mode 100644 index 0000000..7226f83 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/CastOrAuditCallback.java @@ -0,0 +1,52 @@ +package meerkat.voting.controller.callbacks; + +import meerkat.protobuf.Voting.UIElement; +import meerkat.voting.controller.commands.*; +import meerkat.voting.ui.VotingBoothUI.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * A controller callback for the cast-or-audit request to the UI. + * Upon getting a FinalizeBallotChoice response from the voter, the callback then registers a new command + * to the controller queue, either a CastCommand or an AuditCommand according to the voter's choice + */ +public class CastOrAuditCallback extends ControllerCallback { + protected final static Logger logger = LoggerFactory.getLogger(CastOrAuditCallback.class); + protected final UIElement unrecognizedFinalizeResponseMessage; + + public CastOrAuditCallback(int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue, + UIElement unrecognizedFinalizeResponseMessage) { + super(requestId, ballotSerialNumber, controllerQueue); + this.unrecognizedFinalizeResponseMessage = unrecognizedFinalizeResponseMessage; + } + + @Override + public void onSuccess(FinalizeBallotChoice result) { + if (result == FinalizeBallotChoice.CAST) { + enqueueCommand(new CastCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + else if (result == FinalizeBallotChoice.AUDIT) { + enqueueCommand(new AuditCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + else { + logger.error("CastOrAuditCallback got an unrecognized response: " + result); + onFailure(new IllegalArgumentException("CastOrAuditCallback got an unknown result (" + result + ")")); + } + + } + + @Override + public void onFailure(Throwable t) { + logger.error("CastOrAuditCallback got a failure: " + t); + enqueueCommand(new ReportErrorCommand(getRequestIdentifier(), + getBallotSerialNumber(), + unrecognizedFinalizeResponseMessage)); + } +} + + + diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/ChannelChoiceCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/ChannelChoiceCallback.java new file mode 100644 index 0000000..7b97a82 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/ChannelChoiceCallback.java @@ -0,0 +1,50 @@ +package meerkat.voting.controller.callbacks; + +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.commands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * A controller callback for the channel-choice request to the UI. + * Upon receiving the answers for the channel-choice questions, the callback registers a new ChannelDeterminedCommand + * to the controller queue, so the controller can then process the answers and set the channel. + * If voter cancelled during the process, a cancelling exception is thrown and a RestartVotingCommand is + * registered through the onFailure() method + */ +public class ChannelChoiceCallback extends ControllerCallback> { + protected final static Logger logger = LoggerFactory.getLogger(ChannelChoiceCallback.class); + protected final UIElement unsuccessfulChannelChoiceMessage; + + public ChannelChoiceCallback(int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue, + UIElement unsuccessfulChannelChoiceMessage) { + super(requestId, ballotSerialNumber, controllerQueue); + this.unsuccessfulChannelChoiceMessage = unsuccessfulChannelChoiceMessage; + } + + @Override + public void onSuccess(List result) { + logger.debug("callback for channel choice returned success"); + // register the chosen BallotAnswers to a command in the controller queue + enqueueCommand(new ChannelDeterminedCommand(getRequestIdentifier(), getBallotSerialNumber(), result)); + } + + @Override + public void onFailure(Throwable t) { + if (t instanceof VoterCancelThrowable) { + // voter has cancelled during the UI channel choice process. A VoterCancelThrowable is thrown + logger.debug("ChannelChoiceCallback got a cancellation response"); + enqueueCommand(new RestartVotingCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + else { + logger.error("channel choice initiated a failure: " + t); + enqueueCommand(new ReportErrorCommand(getRequestIdentifier(), + getBallotSerialNumber(), + unsuccessfulChannelChoiceMessage)); + } + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/ControllerCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/ControllerCallback.java new file mode 100644 index 0000000..18719bd --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/ControllerCallback.java @@ -0,0 +1,41 @@ +package meerkat.voting.controller.callbacks; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.voting.controller.commands.ControllerCommand; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * The base (abstract) class of all callbacks for requests sent by the controller to other components (ui, output-device) + * It implements the FutureCallback interface + * Its members are: + * - requestIdentifier - uniquely identifies the request which this callback responds + * - ballotSerialNumber - number of ballot which was currently active when request was sent + * - controllerQueue - so the callback can issue and register a new command to the controller, once the request handling is finished + */ +public abstract class ControllerCallback implements FutureCallback { + + private final int requestIdentifier; + private final long ballotSerialNumber; + private LinkedBlockingQueue controllerQueue; + + protected ControllerCallback (int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue) { + this.requestIdentifier = requestId; + this.ballotSerialNumber = ballotSerialNumber; + this.controllerQueue = controllerQueue; + } + + protected int getRequestIdentifier () { + return requestIdentifier; + } + + protected long getBallotSerialNumber () { + return ballotSerialNumber; + } + + protected void enqueueCommand (ControllerCommand command) { + controllerQueue.add(command); + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/EncryptionFailedCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/EncryptionFailedCallback.java new file mode 100644 index 0000000..55906be --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/EncryptionFailedCallback.java @@ -0,0 +1,45 @@ +package meerkat.voting.controller.callbacks; + +import meerkat.voting.controller.commands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * This is quite a special callback. It is not issued in a normal flow of the voting. + * This callback is made only for a request to the UI to choose handling of failure in encryption. + * When encryption/signature fails the voter is asked in the UI whether to retry or abort. + * This specific callback decides, upon the answer to this request, which command to register in the controller's queue + */ +public class EncryptionFailedCallback extends ControllerCallback { + protected final static Logger logger = LoggerFactory.getLogger(EncryptionFailedCallback.class); + + public EncryptionFailedCallback(int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue) { + super(requestId, ballotSerialNumber, controllerQueue); + } + + @Override + public void onSuccess(Integer result) { + logger.debug("callback for encryption-failed request is initiated successfully"); + int res = result.intValue(); + if (res == 0) { + logger.debug("voter chose to retry encryption"); + enqueueCommand(new RetryEncryptAndCommitBallotCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + else if (res == 1) { + logger.debug("voter chose to abort the vote"); + enqueueCommand(new RestartVotingCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + else { + onFailure(new IllegalArgumentException("EncryptionFailedCallback got an unknown result (" + res + ")")); + } + } + + @Override + public void onFailure(Throwable t) { + logger.error("Error message execution initiated a failure: " + t); + enqueueCommand(new RestartVotingCommand(getRequestIdentifier(), getBallotSerialNumber())); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/ErrorMessageRestartCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/ErrorMessageRestartCallback.java new file mode 100644 index 0000000..4d75c1d --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/ErrorMessageRestartCallback.java @@ -0,0 +1,33 @@ +package meerkat.voting.controller.callbacks; + +import meerkat.voting.controller.commands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * This is quite a special callback. It is not issued in a normal flow of the voting. + * This callback is made only for a request to the UI to show the voter an error message. + * Upon approval of the voter, the method onSuccess() of this callback is called, and the voting + * is reset through a command to the controller's queue + */ +public class ErrorMessageRestartCallback extends ControllerCallback { + protected final static Logger logger = LoggerFactory.getLogger(ErrorMessageRestartCallback.class); + + public ErrorMessageRestartCallback(int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue) { + super(requestId, ballotSerialNumber, controllerQueue); + } + + @Override + public void onSuccess(Integer i) { + enqueueCommand(new RestartVotingCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + + @Override + public void onFailure(Throwable t) { + logger.error("Error message execution initiated a failure: " + t); + enqueueCommand(new RestartVotingCommand(getRequestIdentifier(), getBallotSerialNumber())); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/NewVoterCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/NewVoterCallback.java new file mode 100644 index 0000000..17f67be --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/NewVoterCallback.java @@ -0,0 +1,34 @@ +package meerkat.voting.controller.callbacks; + +import meerkat.voting.controller.commands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.concurrent.LinkedBlockingQueue; + + +/** + * A controller callback for the StartSession request to the UI. + * Upon approval of the voter, it registers a new ChannelChoiceCommand to the controller queue (which + * then starts the channel choice process) + */ +public class NewVoterCallback extends ControllerCallback { + protected final static Logger logger = LoggerFactory.getLogger(NewVoterCallback.class); + + public NewVoterCallback(int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue) { + super(requestId, ballotSerialNumber, controllerQueue); + } + + @Override + public void onSuccess(Void v) { + logger.debug("callback for new voting returned success"); + enqueueCommand(new ChannelChoiceCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + + @Override + public void onFailure(Throwable t) { + logger.error("New voting session got a failure: " + t); + enqueueCommand(new RestartVotingCommand(getRequestIdentifier(), getBallotSerialNumber())); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/OutputDeviceCommitCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/OutputDeviceCommitCallback.java new file mode 100644 index 0000000..dd20ec2 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/OutputDeviceCommitCallback.java @@ -0,0 +1,39 @@ +package meerkat.voting.controller.callbacks; + +import meerkat.protobuf.Voting.UIElement; +import meerkat.voting.controller.commands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * A controller callback for the Commit request to the output-device. + * When committing is done, the callback's onSuccess() method is called to register a new ChooseFinalizeOptionCommand + * to the controller + */ +public class OutputDeviceCommitCallback extends ControllerCallback { + protected final static Logger logger = LoggerFactory.getLogger(OutputDeviceCommitCallback.class); + protected final UIElement outputDeviceFailureMessage; + + public OutputDeviceCommitCallback(int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue, + UIElement outputDeviceFailureMessage) { + super(requestId, ballotSerialNumber, controllerQueue); + this.outputDeviceFailureMessage = outputDeviceFailureMessage; + } + + @Override + public void onSuccess(Void v) { + logger.debug("callback for output device commit success"); + enqueueCommand(new ChooseFinalizeOptionCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + + @Override + public void onFailure(Throwable t) { + logger.error("OutputDeviceCommitCallback got a failure: " + t); + enqueueCommand(new ReportErrorCommand(getRequestIdentifier(), + getBallotSerialNumber(), + outputDeviceFailureMessage)); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/OutputDeviceFinalizeCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/OutputDeviceFinalizeCallback.java new file mode 100644 index 0000000..6c5b0d4 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/OutputDeviceFinalizeCallback.java @@ -0,0 +1,39 @@ +package meerkat.voting.controller.callbacks; + +import meerkat.protobuf.Voting.UIElement; +import meerkat.voting.controller.commands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * A controller callback for the Finalize request to the output-device. + * When finalizing (either cast or audit) is done, + * the callback's onSuccess() method is called to register a new command to the controller to restart the voting process + */ +public class OutputDeviceFinalizeCallback extends ControllerCallback { + protected final static Logger logger = LoggerFactory.getLogger(OutputDeviceFinalizeCallback.class); + protected final UIElement outputDeviceFailureMessage; + + public OutputDeviceFinalizeCallback(int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue, + UIElement outputDeviceFailureMessage) { + super(requestId, ballotSerialNumber, controllerQueue); + this.outputDeviceFailureMessage = outputDeviceFailureMessage; + } + + @Override + public void onSuccess(Void v) { + logger.debug("callback for output device finalize success"); + enqueueCommand(new RestartVotingCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + + @Override + public void onFailure(Throwable t) { + logger.error("OutputDeviceFinalizeCallback got a failure: " + t); + enqueueCommand(new ReportErrorCommand(getRequestIdentifier(), + getBallotSerialNumber(), + outputDeviceFailureMessage)); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/VoterCancelThrowable.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/VoterCancelThrowable.java new file mode 100644 index 0000000..38c2c8b --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/VoterCancelThrowable.java @@ -0,0 +1,8 @@ +package meerkat.voting.controller.callbacks; + +/** + * Just a simple unique exception to throw when a voter aborts/cancels the voting during the voting process + */ +public class VoterCancelThrowable extends Throwable { + // +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/VotingCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/VotingCallback.java new file mode 100644 index 0000000..96c940b --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/VotingCallback.java @@ -0,0 +1,48 @@ +package meerkat.voting.controller.callbacks; + +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.commands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * A controller callback for the race-voting request to the UI. + * Upon receiving the answers for the race questions, the callback registers a new command to process + * the voter's answers (encrypt and then commit) into the controller's queue. + * If voter cancelled during the process, a cancelling exception is thrown and a RestartVotingCommand is + * registered through the onFailure() method + */ +public class VotingCallback extends ControllerCallback> { + protected final static Logger logger = LoggerFactory.getLogger(VotingCallback.class); + protected final UIElement unsuccessfulVotingMessage; + + public VotingCallback(int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue, + UIElement unsuccessfulVotingMessage) { + super(requestId, ballotSerialNumber, controllerQueue); + this.unsuccessfulVotingMessage = unsuccessfulVotingMessage; + } + + @Override + public void onSuccess(List result) { + logger.debug("callback for voting returned success"); + enqueueCommand(new EncryptAndCommitBallotCommand(getRequestIdentifier(), getBallotSerialNumber(), result)); + } + + @Override + public void onFailure(Throwable t) { + if (t instanceof VoterCancelThrowable) { + logger.debug("VotingCallback got a cancellation response"); + enqueueCommand(new RestartVotingCommand(getRequestIdentifier(), getBallotSerialNumber())); + } + else { + logger.error("voting initiated a failure: " + t); + enqueueCommand(new ReportErrorCommand(getRequestIdentifier(), + getBallotSerialNumber(), + unsuccessfulVotingMessage)); + } + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/callbacks/WaitForFinishCallback.java b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/WaitForFinishCallback.java new file mode 100644 index 0000000..f3f0308 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/callbacks/WaitForFinishCallback.java @@ -0,0 +1,37 @@ +package meerkat.voting.controller.callbacks; + +import meerkat.protobuf.Voting.UIElement; +import meerkat.voting.controller.commands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * This callback is attached to requests to UI which ask the voter to wait for some process to finish. + * It actually asks nothing in the UI, and it is simply attached to the UI request as a place-holder. + * Therefore its onSuccess() method is empty + */ +public class WaitForFinishCallback extends ControllerCallback { + protected final static Logger logger = LoggerFactory.getLogger(WaitForFinishCallback.class); + protected final UIElement somethingWrongMessage; + + public WaitForFinishCallback(int requestId, + long ballotSerialNumber, + LinkedBlockingQueue controllerQueue, + UIElement somethingWrongMessage) { + super(requestId, ballotSerialNumber, controllerQueue); + this.somethingWrongMessage = somethingWrongMessage; + } + + @Override + public void onSuccess(Void v) { + } + + @Override + public void onFailure(Throwable t) { + logger.error("WaitForFinishCallback got a failure: " + t); + enqueueCommand(new ReportErrorCommand(getRequestIdentifier(), + getBallotSerialNumber(), + somethingWrongMessage)); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/AuditCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/AuditCommand.java new file mode 100644 index 0000000..fa7fc4a --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/AuditCommand.java @@ -0,0 +1,10 @@ +package meerkat.voting.controller.commands; + +/** + * a command to audit the ballot + */ +public class AuditCommand extends ControllerCommand { + public AuditCommand(int requestIdentifier, long ballotSerialNumber) { + super(requestIdentifier, ballotSerialNumber); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/CastCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/CastCommand.java new file mode 100644 index 0000000..c4e96bc --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/CastCommand.java @@ -0,0 +1,10 @@ +package meerkat.voting.controller.commands; + +/** + * a command to cast the ballot + */ +public class CastCommand extends ControllerCommand { + public CastCommand(int requestIdentifier, long ballotSerialNumber) { + super(requestIdentifier, ballotSerialNumber); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/ChannelChoiceCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/ChannelChoiceCommand.java new file mode 100644 index 0000000..9d86edb --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/ChannelChoiceCommand.java @@ -0,0 +1,10 @@ +package meerkat.voting.controller.commands; + +/** + * a command to initiate the channel choice flow at the beginning of the voting + */ +public class ChannelChoiceCommand extends ControllerCommand { + public ChannelChoiceCommand(int requestIdentifier, long ballotSerialNumber) { + super(requestIdentifier, ballotSerialNumber); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/ChannelDeterminedCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/ChannelDeterminedCommand.java new file mode 100644 index 0000000..c9b7023 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/ChannelDeterminedCommand.java @@ -0,0 +1,16 @@ +package meerkat.voting.controller.commands; + +import meerkat.protobuf.Voting.*; +import java.util.List; + +/** + * This command is registered in the controller right after the voter answered all the channel choice questions + */ +public class ChannelDeterminedCommand extends ControllerCommand { + public List channelChoiceAnswers; + + public ChannelDeterminedCommand(int requestIdentifier, long ballotSerialNumber, List answers) { + super(requestIdentifier, ballotSerialNumber); + channelChoiceAnswers = answers; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/ChooseFinalizeOptionCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/ChooseFinalizeOptionCommand.java new file mode 100644 index 0000000..0cdbacc --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/ChooseFinalizeOptionCommand.java @@ -0,0 +1,10 @@ +package meerkat.voting.controller.commands; + +/** + * a command to initiate asking the voter how to finalize (cast-or-audit) the ballot + */ +public class ChooseFinalizeOptionCommand extends ControllerCommand { + public ChooseFinalizeOptionCommand(int requestIdentifier, long ballotSerialNumber) { + super(requestIdentifier, ballotSerialNumber); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/ControllerCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/ControllerCommand.java new file mode 100644 index 0000000..7743723 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/ControllerCommand.java @@ -0,0 +1,23 @@ +package meerkat.voting.controller.commands; + +/** + * This is the base class for the controller commands. + * These commands are registered in a command queue of the controller. + */ +public abstract class ControllerCommand { + protected final int requestIdentifier; + protected final long ballotSerialNumber; + + protected ControllerCommand(int requestIdentifier, long ballotSerialNumber) { + this.requestIdentifier = requestIdentifier; + this.ballotSerialNumber = ballotSerialNumber; + } + + public long getBallotSerialNumber () { + return this.ballotSerialNumber; + } + + public int getRequestIdentifier () { + return this.requestIdentifier; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/EncryptAndCommitBallotCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/EncryptAndCommitBallotCommand.java new file mode 100644 index 0000000..3917a31 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/EncryptAndCommitBallotCommand.java @@ -0,0 +1,24 @@ +package meerkat.voting.controller.commands; + +import meerkat.protobuf.Voting.BallotAnswer; +import java.util.List; + +/** + * a command registered after voter answered all ballot questions. + * The controller then initiates an encryption-signature-commit flow + */ +public class EncryptAndCommitBallotCommand extends ControllerCommand { + private final List votingAnswers; + + public EncryptAndCommitBallotCommand(int requestIdentifier, + long ballotSerialNumber, + List answers) { + super(requestIdentifier, ballotSerialNumber); + votingAnswers = answers; + } + + public List getVotingAnswers() { + return votingAnswers; + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/ReportErrorCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/ReportErrorCommand.java new file mode 100644 index 0000000..fe3e369 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/ReportErrorCommand.java @@ -0,0 +1,20 @@ +package meerkat.voting.controller.commands; + +import meerkat.protobuf.Voting.*; + +/** + * This command is not a part of the normal flow of the controller. + * It asks the controller to handle (report to voter) some error message + */ +public class ReportErrorCommand extends ControllerCommand { + private final UIElement errorMessage; + + public ReportErrorCommand(int requestIdentifier, long ballotSerialNumber, UIElement errorMessage) { + super(requestIdentifier, ballotSerialNumber); + this.errorMessage = errorMessage; + } + + public UIElement getErrorMessage() { + return errorMessage; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/RestartVotingCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/RestartVotingCommand.java new file mode 100644 index 0000000..600a163 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/RestartVotingCommand.java @@ -0,0 +1,11 @@ +package meerkat.voting.controller.commands; + + +/** + * a command to restart a voting flow (for a new voter) + */ +public class RestartVotingCommand extends ControllerCommand { + public RestartVotingCommand(int requestIdentifier, long ballotSerialNumber) { + super(requestIdentifier, ballotSerialNumber); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/commands/RetryEncryptAndCommitBallotCommand.java b/voting-booth/src/main/java/meerkat/voting/controller/commands/RetryEncryptAndCommitBallotCommand.java new file mode 100644 index 0000000..b79a55f --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/commands/RetryEncryptAndCommitBallotCommand.java @@ -0,0 +1,15 @@ +package meerkat.voting.controller.commands; + +/** + * This is quite a special command not part of the normal voting flow. + * It extends the base EncryptAndCommitBallotCommand for occasions where first attempt of encryption failed + * and the voter asks to re-try encrypting and committing. + */ +public class RetryEncryptAndCommitBallotCommand extends EncryptAndCommitBallotCommand { + + public RetryEncryptAndCommitBallotCommand(int requestIdentifier, + long ballotSerialNumber) { + super(requestIdentifier, ballotSerialNumber, null); + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/selector/QuestionSelector.java b/voting-booth/src/main/java/meerkat/voting/controller/selector/QuestionSelector.java new file mode 100644 index 0000000..6603b62 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/selector/QuestionSelector.java @@ -0,0 +1,29 @@ +package meerkat.voting.controller.selector; + +import meerkat.protobuf.Voting.*; +import java.util.List; + +/** + * An interface for the question-selection component. + * This component handles the connection between the channel choice questions and the race questions. + * It gets the answers for the channel choice questions and determines which race question to put in the voter's ballot. + * It also creates an identifier for this chosen channel. This identifier should appear in the plaintext of the ballot. + * The channel identifier does not identify a specific voter, but rather it identifies a specific voting channel + */ +public interface QuestionSelector { + + /** + * determines an identifier for the channel of the voter + * @param channelChoiceAnswers The answers given by the voter to the channel choice questions + * @return an identifier of the channel. To be used by selectQuestionsForVoter(). This identifier should also appear on the plaintext of the ballot + */ + public byte[] getChannelIdentifier (List channelChoiceAnswers); + + /** + * determines which race questions to present to the voter according to its channel + * @param channelIdentifier the identifier of this specific channel + * @return the race questions (to present to the voter) + */ + public List selectQuestionsForVoter (byte[] channelIdentifier); + +} diff --git a/voting-booth/src/main/java/meerkat/voting/controller/selector/SimpleListCategoriesSelector.java b/voting-booth/src/main/java/meerkat/voting/controller/selector/SimpleListCategoriesSelector.java new file mode 100644 index 0000000..6b8f78b --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/controller/selector/SimpleListCategoriesSelector.java @@ -0,0 +1,176 @@ +package meerkat.voting.controller.selector; + +import meerkat.protobuf.Voting.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.lang.Math; + +/** + * A simple implementation of a QuestionSelector. + * This implementation simply regards every single answer in the channel choice phase as an identifier of a category + * Every category is an array of ballot race questions. + * Data of categories is initialized and stored by a SimpleCategoriesSelectionData protobuf. + * After receiving the answers from a channel choice phase, this class simply gathers all the categories + * chosen and compiles the list of ballot questions to include in the ballot for this voter (a question + * is included in the ballot if its index appears in any chosen category, or in the default category shared by all voters) + */ +public class SimpleListCategoriesSelector implements QuestionSelector { + protected final static Logger logger = LoggerFactory.getLogger(SimpleListCategoriesSelector.class); + + // all the possible race questions + private final BallotQuestion[] allBallotQuestions; + + // this category is presented to any voter (regardless of his answers to the channel choice questions) + private final int[] sharedDefaults; + + // all the categories. + // first index is the channel choice question number + // second index is a possible answer to this question + // categoryChoosers[questionNumber][answerNumber] is an array of indices (to the ballotQuestions array). + // This category of questions is included in the ballot if voter answered this specific answer to this channel choice question + private final int[][][] categoryChoosers; + + + private final static byte QUESTION_SELECTED = (byte)1; + private final static byte QUESTION_NOT_SELECTED = (byte)0; + + + /** + * A very straight-forward constructor for the SimpleListCategoriesSelector + * @param allBallotQuestions all possible race questions for this election + * @param data a protobuf containing all the index categories + */ + public SimpleListCategoriesSelector(List allBallotQuestions, SimpleCategoriesSelectionData data) { + // copies the ballot race question list into a member array + this.allBallotQuestions = new BallotQuestion[allBallotQuestions.size()]; + allBallotQuestions.toArray(this.allBallotQuestions); + + // copies the shared category list (as appears in the protobuf data) into a member array + sharedDefaults = listToIntArray(data.getSharedDefaults().getQuestionIndexList()); + + // copies the category lists (as appear in the protobuf data) into a 3-dimensional member array + int[][][] selectionDataTmp = new int[data.getCategoryChooserList().size()][][]; + int channelChoiceQuestionNumber = 0; + for (CategoryChooser catChooser: data.getCategoryChooserList()) { + selectionDataTmp[channelChoiceQuestionNumber] = new int[catChooser.getCategoryList().size()][]; + int channelChoiceAnswerNumber = 0; + for (Category category: catChooser.getCategoryList()) { + selectionDataTmp[channelChoiceQuestionNumber][channelChoiceAnswerNumber] = listToIntArray(category.getQuestionIndexList()); + ++channelChoiceAnswerNumber; + } + ++channelChoiceQuestionNumber; + } + categoryChoosers = selectionDataTmp; + + // verifies in advance that there are not very suspicious indices in the selection data + assertDataValid(); + } + + /** + * asserts that the selection data does not contain a question index which is beyond the length of + * the ballot race questions array. Otherwise, throws an IndexOutOfBoundsException + */ + private void assertDataValid () { + // find the maximum question index in the selection data + int maxQuestionIndex = -1; + for (int index: sharedDefaults) { + maxQuestionIndex = Math.max(maxQuestionIndex, index); + } + for (int[][] categoryChooser: categoryChoosers) { + for (int[] category: categoryChooser) { + for (int index: category) { + maxQuestionIndex = Math.max(maxQuestionIndex, index); + } + } + } + + // asserts that the maximal question index in the selection data does not overflow the ballot race questions array + int questionsLength = allBallotQuestions.length; + if (maxQuestionIndex >= questionsLength) { + String errorMessage = "Selection data refers to question index " + maxQuestionIndex + " while we have only " + questionsLength + " questions totally"; + logger.error(errorMessage); + throw new IndexOutOfBoundsException(errorMessage); + } + } + + + /** + * an implementation of the QuestionSelector interface method. + * In this selector class the identifier simply marks all the ballot race questions which appear in at least one + * category of the categories chosen by the voter (or in the shared defaults category) in the channel choice round. + * @param channelChoiceAnswers The answers given by the voter to the channel choice questions + * @return the channel identifier + */ + @Override + public byte[] getChannelIdentifier(List channelChoiceAnswers) { + /* + * Currently, this implementation of the QuestionSelector interface returns an over-simplified identifier which + * is merely an array of booleans (which flags the questions to appear in the ballot) + * For elections with more than one possible channel we should return a more printable and recognizable + * identifier to be put in the plaintext of the ballot + */ + byte[] isSelected = new byte[allBallotQuestions.length]; + java.util.Arrays.fill(isSelected, QUESTION_NOT_SELECTED); + + for (int i: sharedDefaults) { + isSelected[i] = QUESTION_SELECTED; + } + + int channelChoiceQuestionNumber = 0; + for (BallotAnswer ballotAnswer: channelChoiceAnswers) { + assertAnswerLengthIsOne(ballotAnswer, channelChoiceQuestionNumber); + for (int i: categoryChoosers[channelChoiceQuestionNumber][(int)ballotAnswer.getAnswer(0)]) { + isSelected[i] = QUESTION_SELECTED; + } + } + + return isSelected; + } + + /** + * Verifies that the ballot answer is of length 1. (We do not yet handle multi-choice questions in the channel choice round). + * Otherwise, throws an exception. + * @param ballotAnswer the answer to verify whose length is one + * @param questionNumber the number of the question (needed only for error message strings) + */ + private void assertAnswerLengthIsOne (BallotAnswer ballotAnswer, int questionNumber) { + if (ballotAnswer.getAnswerCount() != 1) { + String errorMessage = "SimpleListCategoriesSelector expects a single answer for every channel choice question\n"; + errorMessage += "Answer to question number " + (questionNumber+1) + " is"; + for (long i : ballotAnswer.getAnswerList()) { + errorMessage += " " + i; + } + logger.error(errorMessage); + throw new IllegalArgumentException(errorMessage); + } + } + + @Override + public List selectQuestionsForVoter(byte[] channelIdentifier) { + List selectedQuestions = new ArrayList<>(); + for (int i = 0; i < channelIdentifier.length; ++i) { + if (channelIdentifier[i] == QUESTION_SELECTED) { + selectedQuestions.add(allBallotQuestions[i]); + } + } + return selectedQuestions; + } + + /** + * copies a List of Integers into an int[] array of same length + * @param l a list of Integers + * @return an array of ints + */ + private int[] listToIntArray(List l) { + int[] res = new int[l.size()]; + int index = 0; + for (Integer i: l) { + res[index] = i; + ++index; + } + return res; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/encryptor/VBCryptoManager.java b/voting-booth/src/main/java/meerkat/voting/encryptor/VBCryptoManager.java new file mode 100644 index 0000000..04416f3 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/encryptor/VBCryptoManager.java @@ -0,0 +1,44 @@ +package meerkat.voting.encryptor; + +import meerkat.protobuf.Voting.*; + +import java.io.IOException; +import java.security.SignatureException; + +/** + * An interface for the encryptor component of the voting booth + * It handles both the encryption and the digital signature + */ +public interface VBCryptoManager { + /** + * A simple class for pairing EncrypedBallot together with its matching BallotSecrets + */ + public class EncryptionAndSecrets { + private final SignedEncryptedBallot signedEncryptedBallot; + private final BallotSecrets secrets; + + public EncryptionAndSecrets (SignedEncryptedBallot encryptedBallot, BallotSecrets secrets) { + this.signedEncryptedBallot = encryptedBallot; + this.secrets = secrets; + } + + public SignedEncryptedBallot getSignedEncryptedBallot() { + return signedEncryptedBallot; + } + + public BallotSecrets getSecrets() { + return secrets; + } + } + + + /** + * This function encrypts the plaintext ballot using the booth's keys + * @param plaintextBallot - all plaintext ballot info of the voter + * @return an encryption of the ballot + */ + // TODO: do we seed the random here? + public EncryptionAndSecrets encrypt (PlaintextBallot plaintextBallot) throws SignatureException, IOException; + + +} diff --git a/voting-booth/src/main/java/meerkat/voting/encryptor/VBCryptoManagerImpl.java b/voting-booth/src/main/java/meerkat/voting/encryptor/VBCryptoManagerImpl.java new file mode 100644 index 0000000..83fb8b0 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/encryptor/VBCryptoManagerImpl.java @@ -0,0 +1,68 @@ +package meerkat.voting.encryptor; + +import meerkat.crypto.*; +import meerkat.protobuf.Crypto.*; +import meerkat.protobuf.Voting.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.security.SignatureException; +import java.util.Random; + +/** + * A basic implementation of the VBCryptoManager interface + */ +public class VBCryptoManagerImpl implements VBCryptoManager { + + protected final static Logger logger = LoggerFactory.getLogger(VBCryptoManagerImpl.class); + + private final Random random; //TODO: Random object should be more cryptographycally secure + private final Encryption encryption; + private final DigitalSignature digitalSignature; + + + public VBCryptoManagerImpl (Random rand, Encryption encryption, DigitalSignature digitalSignature) { + this.random = rand; + this.encryption = encryption; + this.digitalSignature = digitalSignature; + } + + + @Override + public EncryptionAndSecrets encrypt(PlaintextBallot plaintextBallot) throws SignatureException, IOException { + + // TODO: do we seed the random here? + + try { + EncryptionRandomness encryptionRandomness = encryption.generateRandomness(random); + BallotSecrets secrets = BallotSecrets.newBuilder() + .setPlaintextBallot(plaintextBallot) + .setEncryptionRandomness(encryptionRandomness) + .build(); + RerandomizableEncryptedMessage encryptedMessage = encryption.encrypt(plaintextBallot, encryptionRandomness); + EncryptedBallot encBallot = EncryptedBallot.newBuilder() + .setSerialNumber(plaintextBallot.getSerialNumber()) + .setData(encryptedMessage) + .build(); + digitalSignature.updateContent(encBallot); + + SignedEncryptedBallot signedEncryptedBallot = SignedEncryptedBallot.newBuilder() + .setEncryptedBallot(encBallot) + .setSignature(digitalSignature.sign()) + .build(); + + // TODO: still has to supply RandomnessGenerationProof as well + + return new EncryptionAndSecrets(signedEncryptedBallot, secrets); + } + catch (IOException e) { + logger.error("encrypt: the encryption component has thrown an exception: " + e); + throw e; + } + catch (SignatureException e) { + logger.error("encrypt: the signature component has thrown an exception: " + e); + throw e; + } + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/output/AsyncRunnableOutputDevice.java b/voting-booth/src/main/java/meerkat/voting/output/AsyncRunnableOutputDevice.java new file mode 100644 index 0000000..0052a2c --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/output/AsyncRunnableOutputDevice.java @@ -0,0 +1,140 @@ +package meerkat.voting.output; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.protobuf.Voting.BallotSecrets; +import meerkat.protobuf.Voting.PlaintextBallot; +import meerkat.protobuf.Voting.SignedEncryptedBallot; +import meerkat.voting.controller.callbacks.ControllerCallback; +import meerkat.voting.controller.callbacks.OutputDeviceCommitCallback; +import meerkat.voting.controller.callbacks.OutputDeviceFinalizeCallback; +import meerkat.voting.output.outputcommands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ArrayBlockingQueue; + +/** + * This is a base class for simple OutputDevices which run asynchronously (as a separate thread). + * The methods of the BallotOutputDevice simply register a matching OutputCommand in the instance's queue + * The Runnable.run method simply takes the next registered command and calls the matching (abstract) method + */ +public abstract class AsyncRunnableOutputDevice implements BallotOutputDevice, Runnable { + + private Logger logger; + private ArrayBlockingQueue queue; + private volatile boolean shutDownHasBeenCalled; + + public AsyncRunnableOutputDevice() { + logger = LoggerFactory.getLogger(AsyncRunnableOutputDevice.class); + logger.info("AsyncRunnableOutputDevice is constructed"); + queue = new ArrayBlockingQueue<>(1); + shutDownHasBeenCalled = false; + } + + @Override + public void run () { + logger.info("starts running"); + while (! wasShutDownCalled()) { + try { + OutputCommand command = queue.take(); + handleSingleCommand(command); + } + catch (InterruptedException e) { + logger.warn("Interrupted while reading from command queue " + e); + } + } + } + + private boolean wasShutDownCalled () { + return shutDownHasBeenCalled; + } + + @Override + public void callShutDown() { + logger.info("callShutDown command has been called"); + shutDownHasBeenCalled = true; + queue.clear(); + } + + /** + * chooses the next method to run according to the type of the given OutputCommand + * @param command any valid OutputCommand + */ + private void handleSingleCommand(OutputCommand command) { + if (command instanceof CommitOutputCommand) { + doCommitToBallot((CommitOutputCommand)command); + } + else if (command instanceof AuditOutputCommand) { + doAudit((AuditOutputCommand)command); + } + else if (command instanceof CastOutputCommand) { + doCastBallot((CastOutputCommand)command); + } + else if (command instanceof CancelOutputCommand) { + doCancel((CancelOutputCommand)command); + } + else { + String errorMessage = "handleSingleCommand: unknown type of OutputCommand received: " + + command.getClass().getName(); + logger.error(errorMessage); + throw new RuntimeException(errorMessage); + } + } + + + @Override + public void commitToBallot(PlaintextBallot plaintextBallot, + SignedEncryptedBallot signedEncryptedBallot, + FutureCallback callback) { + logger.debug("Output interface call to commit to ballot"); + queue.clear(); + queue.add(new CommitOutputCommand(plaintextBallot, signedEncryptedBallot, (OutputDeviceCommitCallback)callback)); + } + + @Override + public void audit(BallotSecrets ballotSecrets, FutureCallback callback) { + logger.debug("an interface call to audit"); + queue.clear(); + queue.add(new AuditOutputCommand(ballotSecrets, (OutputDeviceFinalizeCallback)callback)); + } + + @Override + public void castBallot(FutureCallback callback) { + logger.debug("an interface call to cast ballot"); + queue.clear(); + queue.add(new CastOutputCommand((OutputDeviceFinalizeCallback)callback)); + } + + @Override + public void cancelBallot(FutureCallback callback) { + logger.debug("an interface call to cancel the output"); + queue.clear(); + queue.add(new CancelOutputCommand((ControllerCallback)callback)); + } + + + /** + * This method should be filled by an extending class. It should have the details of how to commit to a ballot + * @param command a CommitOutputCommand with the details and the callback + */ + abstract void doCommitToBallot(CommitOutputCommand command); + + /** + * This method should be filled by an extending class. It should have the details of how to audit the ballot + * @param command a AuditOutputCommand with the details and the callback + */ + abstract void doAudit(AuditOutputCommand command); + + /** + * This method should be filled by an extending class. It should have the details of how to cast the ballot + * @param command a CastOutputCommand with the details and the callback + */ + abstract void doCastBallot(CastOutputCommand command); + + /** + * This method should be filled by an extending class. It should have the details of how to cancel the ballot output + * @param command a CancelOutputCommand with the details and the callback + */ + abstract void doCancel(CancelOutputCommand command); + +} diff --git a/voting-booth/src/main/java/meerkat/voting/output/BallotOutputDevice.java b/voting-booth/src/main/java/meerkat/voting/output/BallotOutputDevice.java new file mode 100644 index 0000000..3909ce3 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/output/BallotOutputDevice.java @@ -0,0 +1,44 @@ +package meerkat.voting.output; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.protobuf.Voting.*; + +/** + * An interface for the device in which we output the ballots. + * Probably going to be a printer or an ethernet connection, or both. + */ +public interface BallotOutputDevice { + + /** + * Output the encrypted ballot. This is a commitment before voter chooses casting or auditing + * @param encryptedBallot - the encrypted ballot to commit to + * @param callback - a callback object which expects no return value + */ + public void commitToBallot(PlaintextBallot plaintextBallot, + SignedEncryptedBallot encryptedBallot, + FutureCallback callback); + + /** + * Voter chose 'audit'. Output the ballot secrets to prove correctness of the encryption. + * @param ballotSecrets - the secrets of the encryption + * @param callback - a callback object which expects no return value + */ + public void audit(BallotSecrets ballotSecrets, FutureCallback callback); + + /** + * Voter chose 'cast'. Finalize the ballot for use in the polling station + * @param callback - a callback object which expects no return value + */ + public void castBallot(FutureCallback callback); + + /** + * Cancelling the current ballot. This clears the state of the OutputDevice if the implementation has any such state. + * @param callback - a callback object which expects no return value + */ + public void cancelBallot(FutureCallback callback); + + /** + * A method for shutting down the Output Device + */ + public void callShutDown(); +} diff --git a/voting-booth/src/main/java/meerkat/voting/output/NetworkVirtualPrinter.java b/voting-booth/src/main/java/meerkat/voting/output/NetworkVirtualPrinter.java new file mode 100644 index 0000000..74f9970 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/output/NetworkVirtualPrinter.java @@ -0,0 +1,108 @@ +package meerkat.voting.output; + +import com.google.protobuf.BoolValue; +import com.google.protobuf.ByteString; +import meerkat.protobuf.PollingStation.ScannedData; +import meerkat.protobuf.Voting.SignedEncryptedBallot; +import meerkat.rest.Constants; +import meerkat.rest.ProtobufMessageBodyReader; +import meerkat.rest.ProtobufMessageBodyWriter; +import meerkat.voting.output.outputcommands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.client.*; +import javax.ws.rs.core.Response; +import java.io.IOException; + +import static meerkat.pollingstation.PollingStationConstants.POLLING_STATION_WEB_SCANNER_SCAN_PATH; + +/** + * A ballot output device for the network. It simply sends details over the wire + */ +public class NetworkVirtualPrinter extends AsyncRunnableOutputDevice { + + private static final Logger logger = LoggerFactory.getLogger(NetworkVirtualPrinter.class); + private ByteString channelIdentifier; + private SignedEncryptedBallot signedEncryptedBallot; + private final WebTarget successfulPrintTarget; + + public NetworkVirtualPrinter(String address) { + super(); + logger.info("A NetworkVirtualPrinter is constructed"); + Client client = ClientBuilder.newClient(); + client.register(ProtobufMessageBodyReader.class); + client.register(ProtobufMessageBodyWriter.class); + successfulPrintTarget = client.target(address).path(POLLING_STATION_WEB_SCANNER_SCAN_PATH); + resetState(); + } + + + /** + * The NetworkVirtualPrinter actually does nothing for committing. + * It simply keeps the ballot details for later. + * When the voter chooses to Cast the ballot, these details are sent over the wire. + * @param command a CommitOutputCommand with the signed encryption of the ballot + */ + public void doCommitToBallot(CommitOutputCommand command) { + logger.debug("entered method doCommitToBallot"); + channelIdentifier = command.getChannelIdentifierByteString(); + signedEncryptedBallot = command.getSignedEncryptedBallot(); + command.getCallback().onSuccess(null); + } + + + /** + * The NetworkVirtualPrinter actually does nothing for auditing. + * @param command a AuditOutputCommand with the details and the callback + */ + public void doAudit(AuditOutputCommand command) { + logger.debug("entered method doAudit"); + resetState(); + command.getCallback().onSuccess(null); + } + + + /** + * This is where the magic happens. The signed encrypted ballot is transmitted over the wire + * @param command a CastOutputCommand with the details and the callback + */ + public void doCastBallot(CastOutputCommand command) { + logger.debug("entered method doCastBallot"); + ScannedData scannedData = ScannedData.newBuilder() + .setChannel(channelIdentifier) + .setSignedEncryptedBallot(this.signedEncryptedBallot) + .build(); + + Response response = successfulPrintTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(scannedData, Constants.MEDIATYPE_PROTOBUF)); + BoolValue b = response.readEntity(BoolValue.class); + response.close(); + + resetState(); + + if (b.getValue()) { + command.getCallback().onSuccess(null); + } + else { + command.getCallback().onFailure(new IOException()); + } + } + + + /** + * The NetworkVirtualPrinter actually does nothing for canceling. + * @param command a CancelOutputCommand with the callback + */ + public void doCancel(CancelOutputCommand command) { + logger.debug("entered method doCancel"); + resetState(); + command.getCallback().onSuccess(null); + } + + + private void resetState() { + channelIdentifier = null; + signedEncryptedBallot = null; + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/output/SystemConsoleOutputDevice.java b/voting-booth/src/main/java/meerkat/voting/output/SystemConsoleOutputDevice.java new file mode 100644 index 0000000..4f50c27 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/output/SystemConsoleOutputDevice.java @@ -0,0 +1,106 @@ +package meerkat.voting.output; + +import com.google.protobuf.ByteString; +import meerkat.protobuf.Crypto.*; +import meerkat.protobuf.Voting.*; +import meerkat.voting.output.outputcommands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A toy OutputDevice class + * outputs everything simply to the System console + */ +public class SystemConsoleOutputDevice extends AsyncRunnableOutputDevice { + + private static final Logger logger = LoggerFactory.getLogger(SystemConsoleOutputDevice.class); + + public SystemConsoleOutputDevice () { + super(); + logger.info("A SystemConsoleOutputDevice is constructed"); + } + + /** + * Committing to the ballot. + * Simply prints to the output stream all the details in the CommitOutputCommand. + * @param command details to commit to, and the callback to call when finished + */ + public void doCommitToBallot(CommitOutputCommand command) { + logger.debug("entered method doCommitToBallot"); + PlaintextBallot plaintextBallot = command.getPlaintext(); + long plaintextSerialNumber = plaintextBallot.getSerialNumber(); + System.out.println("Commitment of Ballot #" + plaintextSerialNumber); + System.out.println("(channel): "); + System.out.println(bytesToString(command.getChannelIdentifierByteString())); + System.out.println("(plaintext): "); + System.out.println(plaintextBallot); + SignedEncryptedBallot signedEncryptedBallot = command.getSignedEncryptedBallot(); + long encryptedSerialNumber = signedEncryptedBallot.getEncryptedBallot().getSerialNumber(); + System.out.println("Commitment of Ballot #" + encryptedSerialNumber + " (ciphertext):"); + if (plaintextSerialNumber != encryptedSerialNumber) { + logger.error("plaintext and encryption serial numbers do not match!! plaintext# = " + + plaintextSerialNumber + ", ciphertext# = " + encryptedSerialNumber); + } + ByteString encryptedData = signedEncryptedBallot.getEncryptedBallot().getData().getData(); + System.out.println(bytesToString(encryptedData)); + command.getCallback().onSuccess(null); + } + + + /** + * auditing the ballot. + * prints to the output stream the ballot secrets (the encryption randomness and its proof of random generation) + * @param command An auditing command with the callback to finally call + */ + public void doAudit(AuditOutputCommand command) { + logger.debug("entered method doAudit"); + System.out.println("Auditing"); + BallotSecrets ballotSecrets = command.getBallotSecrets(); + printEncryptionRandomness(ballotSecrets.getEncryptionRandomness()); + printRandomnessGenerationProof (ballotSecrets.getProof()); + command.getCallback().onSuccess(null); + } + + /** + * Casting the ballot (actually does nothing new) + * @param command a CastOutputCommand with the details and the callback + */ + public void doCastBallot(CastOutputCommand command) { + logger.debug("entered method doCastBallot"); + System.out.println("Ballot finalized for casting!"); + command.getCallback().onSuccess(null); + } + + + /** + * Canceling the ballot (actually does nothing new) + * @param command a CancelOutputCommand with the details and the callback + */ + public void doCancel(CancelOutputCommand command) { + logger.debug("entered method doCancel"); + System.out.println("Ballot cancelled!"); + command.getCallback().onSuccess(null); + } + + + private void printEncryptionRandomness (EncryptionRandomness encryptionRandomness) { + System.out.println("Encryption Randomness = "); + ByteString data = encryptionRandomness.getData(); + System.out.println(bytesToString(data)); + } + + private void printRandomnessGenerationProof (RandomnessGenerationProof proof) { + System.out.println("Proof of randomness generation:"); + ByteString data = proof.getData(); + System.out.println(bytesToString(data)); + } + + + /* + * Returns the UTF8 decoding of byte-string data + */ + private static String bytesToString(ByteString data) { + return data.toStringUtf8(); + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/output/outputcommands/AuditOutputCommand.java b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/AuditOutputCommand.java new file mode 100644 index 0000000..0d4d6ea --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/AuditOutputCommand.java @@ -0,0 +1,22 @@ +package meerkat.voting.output.outputcommands; + +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.callbacks.ControllerCallback; + +/** + * This OutputCommand supplies the necessary details for outputting Audit information + */ +public class AuditOutputCommand extends OutputCommand { + + private final BallotSecrets ballotSecrets; + + public AuditOutputCommand(BallotSecrets ballotSecrets, ControllerCallback callback) { + super(callback); + this.ballotSecrets = ballotSecrets; + } + + public BallotSecrets getBallotSecrets() { + return ballotSecrets; + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/output/outputcommands/CancelOutputCommand.java b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/CancelOutputCommand.java new file mode 100644 index 0000000..88fe03f --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/CancelOutputCommand.java @@ -0,0 +1,14 @@ +package meerkat.voting.output.outputcommands; + +import meerkat.voting.controller.callbacks.ControllerCallback; + +/** + * This OutputCommand signals the output-device that it should Cancel the rest of the ballot output + */ +public class CancelOutputCommand extends OutputCommand { + + public CancelOutputCommand(ControllerCallback callback) { + super(callback); + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/output/outputcommands/CastOutputCommand.java b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/CastOutputCommand.java new file mode 100644 index 0000000..5098cd5 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/CastOutputCommand.java @@ -0,0 +1,14 @@ +package meerkat.voting.output.outputcommands; + +import meerkat.voting.controller.callbacks.ControllerCallback; + +/** + * This OutputCommand signals the output-device that the voter wishes to Cast the ballot + */ +public class CastOutputCommand extends OutputCommand { + + public CastOutputCommand(ControllerCallback callback) { + super(callback); + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/output/outputcommands/CommitOutputCommand.java b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/CommitOutputCommand.java new file mode 100644 index 0000000..42c9307 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/CommitOutputCommand.java @@ -0,0 +1,34 @@ +package meerkat.voting.output.outputcommands; + +import com.google.protobuf.ByteString; +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.callbacks.ControllerCallback; + +/** + * This OutputCommand supplies the necessary details for outputting a commit to the ballot + */ +public class CommitOutputCommand extends OutputCommand { + + private final PlaintextBallot plaintextBallot; + private final SignedEncryptedBallot signedEncryptedBallot; + + public CommitOutputCommand(PlaintextBallot plaintextBallot, + SignedEncryptedBallot signedEncryptedBallot, + ControllerCallback callback) { + super(callback); + this.plaintextBallot = plaintextBallot; + this.signedEncryptedBallot = signedEncryptedBallot; + } + + public ByteString getChannelIdentifierByteString() { + return plaintextBallot.getChannelIdentifier(); + } + + public PlaintextBallot getPlaintext() { + return plaintextBallot; + } + + public SignedEncryptedBallot getSignedEncryptedBallot() { + return signedEncryptedBallot; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/output/outputcommands/OutputCommand.java b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/OutputCommand.java new file mode 100644 index 0000000..89591fb --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/output/outputcommands/OutputCommand.java @@ -0,0 +1,18 @@ +package meerkat.voting.output.outputcommands; + +import meerkat.voting.controller.callbacks.ControllerCallback; + +/** + * Base class for the commands to put in the output-device queue + */ +public abstract class OutputCommand { + protected final ControllerCallback callback; + + protected OutputCommand(ControllerCallback callback) { + this.callback = callback; + } + + public ControllerCallback getCallback () { + return callback; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/storage/StorageManager.java b/voting-booth/src/main/java/meerkat/voting/storage/StorageManager.java new file mode 100644 index 0000000..4a70f79 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/storage/StorageManager.java @@ -0,0 +1,49 @@ +package meerkat.voting.storage; + +import meerkat.protobuf.Voting.*; + +import java.io.IOException; +import java.util.Map; + +/** + * An interface for the storage component of the voting booth + */ +public interface StorageManager { + + /** + * Detect whether an administration key is inserted to the machine. This determines if we gointo the set-up flow or the voting session flow. + * @return True is a hardware key is inserted. False if not. + */ + public boolean isAdminHardwareKeyInserted(); + + /** + * load the election params from the storage. + * @return the current election params + * @throws IOException + */ + public ElectionParams readElectionParams () throws IOException; + + /** + * write the election parameters protobuf to the storage + * @param params ElectionParams protobuf to save + * @throws IOException + */ + public void writeElectionParams(ElectionParams params) throws IOException; + + + public Map readSystemMessages() throws IOException; + + // These are just static key identifiers for accessing the matching System Messages in the message map + public final static String WAIT_FOR_COMMIT_MESSAGE = "waitForCommit"; + public final static String WAIT_FOR_AUDIT_MESSAGE = "waitForAudit"; + public final static String WAIT_FOR_CAST_MESSAGE = "waitForCast"; + public final static String RESTART_VOTING_BUTTON = "restartVotingButton"; + public final static String UNRECOGNIZED_FINALIZE_RESPONSE_MESSAGE = "unrecognizedFinalizeResponse"; + public final static String UNSUCCESSFUL_CHANNEL_CHOICE_MESSAGE = "unsuccessfulChannelChoice"; + public final static String OUTPUT_DEVICE_FAILURE_MESSAGE = "outputDeviceFailure"; + public final static String UNSUCCESSFUL_VOTING_MESSAGE = "unsuccessfulVoting"; + public final static String SOMETHING_WRONG_MESSAGE = "somethingWrong"; + public final static String ENCRYPTION_FAILED_MESSAGE = "encryptionFailed"; + public final static String RETRY_BUTTON = "retryButton"; + public final static String CANCEL_VOTE_BUTTON = "cancelVoteButton"; +} diff --git a/voting-booth/src/main/java/meerkat/voting/storage/StorageManagerMockup.java b/voting-booth/src/main/java/meerkat/voting/storage/StorageManagerMockup.java new file mode 100644 index 0000000..f556e3b --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/storage/StorageManagerMockup.java @@ -0,0 +1,90 @@ +package meerkat.voting.storage; + +import meerkat.protobuf.Voting.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Map; + +/** + * A mockup for the StorageManager interface + * Currently keeps the ElectionParams in a file in the user's home-directory + */ +public class StorageManagerMockup implements StorageManager { + + private static final Logger logger = LoggerFactory.getLogger(StorageManagerMockup.class); + + public static final String electionParamFullFilename = "/home/hai/meerkat-java/meerkat_election_params_tempfile.dat"; + public static final String systemMessagesFilename = "/home/hai/meerkat-java/meerkat_booth_system_messages.dat"; + + private boolean adminHardwareKeyInserted; + + + public StorageManagerMockup () { + logger.info("A StorageManagerMockup is constructed"); + this.adminHardwareKeyInserted = false; + } + + @Override + public boolean isAdminHardwareKeyInserted() { + logger.info("Entered method isAdminHardwareKeyInserted"); + logger.warn("isAdminHardwareKeyInserted is not yet fully implemented. It does not check the file system. " + + "Rather it just returns a private boolean member"); + return adminHardwareKeyInserted; + } + + @Override + public ElectionParams readElectionParams() throws IOException { + logger.info("Entered method readElectionParams"); + ElectionParams params; + try { + FileInputStream inputStream = new FileInputStream(electionParamFullFilename); + params = ElectionParams.parseFrom(inputStream); + inputStream.close(); + logger.info ("Successfully read election parameter protobuf from a file"); + } + catch (IOException e) { + logger.error("Could not read from the election parameter file: '" + electionParamFullFilename + "'."); + throw e; + } + return params; + } + + @Override + public void writeElectionParams(ElectionParams params) throws IOException { + logger.info("Entered method writeElectionParams"); + try { + FileOutputStream output = new FileOutputStream(electionParamFullFilename); + params.writeTo(output); + output.close(); + logger.info ("Successfully wrote election parameter protobuf to a file"); + } + catch (IOException e) { + logger.error("Could not write to the election parameter file: '" + electionParamFullFilename + "'."); + throw e; + } + } + + + @Override + public Map readSystemMessages() throws IOException { + + logger.info("Entered method readSystemMessages"); + BoothSystemMessages systemMessages; + try { + FileInputStream inputStream = new FileInputStream(systemMessagesFilename); + systemMessages = BoothSystemMessages.parseFrom(inputStream); + inputStream.close(); + logger.info ("Successfully read systemMessages protobuf from a file"); + } + catch (IOException e) { + logger.error("Could not read from the systemMessages file: '" + systemMessagesFilename + "'."); + throw e; + } + return systemMessages.getSystemMessage(); + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/CommandPend.java b/voting-booth/src/main/java/meerkat/voting/ui/CommandPend.java new file mode 100644 index 0000000..a12d3ac --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/CommandPend.java @@ -0,0 +1,53 @@ +package meerkat.voting.ui; + +import java.util.concurrent.ArrayBlockingQueue; + +/** + * A special kind of an ArrayBlockingQueue. + * It is only of size 1, meaning it keeps only one element at most at every point of time. + * The trample function is similar to put/add except that it overrides the previously kept element. + * Other functions are similar to the matching functions in ArrayBlockingQueue. + * For instance, the offer function only "recommends" another element to keep, but if there is a stored element + * already, than this recommendation is ignored. + */ +public class CommandPend { + + private ArrayBlockingQueue queue; + + public CommandPend () { + queue = new ArrayBlockingQueue<>(1); + } + + /** + * overrides the current kept command + * @param cmd a command to override the previous one (if existed) + */ + synchronized public void trample (T cmd) { + queue.clear(); + queue.add(cmd); + } + + /** + * keeps the offered command, but only if there is no other command to handle right now + * @param cmd a command to keep if we currently do not have another + */ + synchronized public void offer (T cmd) { + queue.offer(cmd); + } + + /** + * Retrieves and removes the kept command, waiting if necessary until a command becomes available. + * @return the kept command + * @throws InterruptedException + */ + public T take() throws InterruptedException { + return queue.take(); + } + + /** + * removes the kept command + */ + synchronized public void clear () { + queue.clear(); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/SystemConsoleUI.java b/voting-booth/src/main/java/meerkat/voting/ui/SystemConsoleUI.java new file mode 100644 index 0000000..3b34da8 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/SystemConsoleUI.java @@ -0,0 +1,533 @@ +package meerkat.voting.ui; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.protobuf.ByteString; +import meerkat.protobuf.Voting.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.*; + +import meerkat.voting.controller.callbacks.*; +import meerkat.voting.ui.uicommands.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.lang.System.in; + + +/** + * an asynchronous thread implementation of the VotingBoothUI interface + * This is a mock-up implementation using just the console as our UI device + */ +public class SystemConsoleUI implements VotingBoothUI, Runnable { + + private static final Logger logger = LoggerFactory.getLogger(SystemConsoleUI.class); + + private BufferedReader bufferedReader; + private CommandPend cmdPend; + private Date startWaitingTime; + + private volatile boolean shutDownHasBeenCalled; + + + public SystemConsoleUI() { + final int tickDurationInMillisec = 10; // period between view update calls + + logger.info("A VB UI console is constructed"); + cmdPend = new CommandPend<>(); + bufferedReader = new BufferedReader(new InputStreamReader(in)); + + startWaitingTime = null; + Timer timer = new Timer(); + timer.scheduleAtFixedRate(new TickerTimerTask(cmdPend), new Date(), tickDurationInMillisec); + + shutDownHasBeenCalled = false; + } + + + /** + * the run() method. Simply loops and takes the UI's pending command and handles it accordingly + */ + @Override + public void run () { + logger.info("UI starts running"); + while (! wasShutDownCalled()) { + try { + UICommand command = cmdPend.take(); + handleSingleCommand(command); + } + catch (InterruptedException e) { + logger.warn ("Interrupted while reading the pending command " + e); + } + } + } + + + @Override + public void callShutDown() { + logger.info("callShutDown command has been called"); + shutDownHasBeenCalled = true; + stopWaiting(); + cmdPend.clear(); + } + + /** + * chooses the next method to run according to the type of the given UICommand. + * Special case for the TickCommand. + * As this command is registered in the queue constantly, we simply ignore this command if the UI is not in + * a waiting state + * @param command any valid UICommand + */ + private void handleSingleCommand(UICommand command) { + if (!(command instanceof TickCommand)) { + if (startWaitingTime != null) { + stopWaiting(); + } + } + + if (command instanceof StartSessionUICommand) { + doShowWelcomeScreen((StartSessionUICommand)command); + } + else if (command instanceof ChannelChoiceUICommand) { + doAskChannelChoiceQuestions((ChannelChoiceUICommand)command); + } + else if (command instanceof RaceVotingUICommand) { + doAskVotingQuestions((RaceVotingUICommand)command); + } + else if (command instanceof CastOrAuditUICommand) { + doCastOrAudit ((CastOrAuditUICommand)command); + } + else if (command instanceof FatalErrorUICommand) { + doFatalError((FatalErrorUICommand)command); + } + else if (command instanceof WaitForFinishUICommand) { + doWaitForFinish((WaitForFinishUICommand)command); + } + else if (command instanceof TickCommand) { + doTick (); + } + else { + String errorMessage = "handleSingleCommand: unknown type of UICommand received: " + + command.getClass().getName(); + logger.error(errorMessage); + throw new RuntimeException(errorMessage); + } + } + + + /** + * start a new session by registering a StartSessionUICommand + * @param callback - a boolean future callback to return when done + */ + @Override + public void startNewVoterSession(FutureCallback callback) { + logger.debug("UI interface call to startNewVoterSession"); + cmdPend.trample(new StartSessionUICommand((NewVoterCallback)callback)); + } + + /** + * welcomes the new voter at the beginning of the session + * @param command a StartSessionUICommand with a acallback + */ + private void doShowWelcomeScreen(StartSessionUICommand command) { + logger.debug("UI entered doShowWelcomeScreen"); + System.out.println("Welcome, new voter!"); + waitForEnter(null); + ControllerCallback callback = command.getCallback(); + callback.onSuccess(null); + } + + /** + * marks that the waiting, for something else to have happened, is finished + */ + private void stopWaiting () { + System.out.println (); + startWaitingTime = null; + + } + + /** + * waits until the ENTER key is pressed in the console + * @param message a message to show the user in the console + */ + private void waitForEnter(String message) { + if (message != null) { + System.out.println(message); + } + System.out.println("\nPress ENTER to proceed.\n"); + String t = null; + while (t == null) { + try { + t = readInputLine(); + } + catch (IOException e) { + String errorMessage = "waitForEnter: threw an IOException: " + e; + logger.error(errorMessage); + System.err.println(errorMessage); + } + } + } + + /** + * call for the channel choice phase by registering a ChannelChoiceUICommand in the queue + * @param questions questions to determine the right voting channel for this voter + * @param callback that's where we store the answers to decide channel upon for the current voter + */ + @Override + public void chooseChannel(List questions, FutureCallback> callback) { + logger.debug("UI interface call to chooseChannel"); + cmdPend.trample(new ChannelChoiceUICommand(questions, (ChannelChoiceCallback)callback)); + } + + /** + * lists the channel choice questions to the voter and gathers the voter's answers + * @param command a ChannelChoiceUICommand with the data and a callback + */ + private void doAskChannelChoiceQuestions (ChannelChoiceUICommand command) { + logger.debug("UI: doAskChannelChoiceQuestions"); + System.out.println("Showing questions for choosing channel:\n"); + try { + List answers = askVoterForAnswers(command.getQuestions()); + command.getCallback().onSuccess(answers); + } + catch (VoterCancelThrowable e) { + command.getCallback().onFailure(e); + } + catch (IOException e) { + String errorMessage = "Channel choice failed due to IOException: " + e; + logger.error (errorMessage); + System.err.println(errorMessage); + command.getCallback().onFailure(e); + } + } + + /** + * call for the race voting question phase by registering a RaceVotingUICommand in the queue + * @param questions all ballot questions to present to the voter + * @param callback the responses to the questions collected by the UI, to send back to the controller. Responses are null if voter chose to cancel session + */ + @Override + public void askVoterQuestions(List questions, FutureCallback> callback) { + logger.debug("UI interface call to chooseChannel"); + cmdPend.trample(new RaceVotingUICommand(questions, (VotingCallback)callback)); + } + + /** + * lists the race voting questions to the voter and gathers the voter's answers + * @param command a RaceVotingUICommand with a callback + */ + private void doAskVotingQuestions (RaceVotingUICommand command) { + logger.debug("UI: doAskVotingQuestions"); + System.out.println("Showing questions for race voting:\n"); + try { + List answers = askVoterForAnswers(command.getQuestions()); + command.getCallback().onSuccess(answers); + } + catch (VoterCancelThrowable e) { + command.getCallback().onFailure(e); + } + catch (IOException e) { + String errorMessage = "Asking voting questions failed due to IOException: " + e; + logger.error (errorMessage); + System.err.println(errorMessage); + command.getCallback().onFailure(e); + } + } + + /** + * call for the cast-or-audit phase by registering a CastOrAuditUICommand in the queue + * @param callback the returned choice of how to finalize the ballot + */ + @Override + public void castOrAudit(FutureCallback callback) { + logger.debug("UI interface call to castOrAudit"); + cmdPend.trample(new CastOrAuditUICommand((CastOrAuditCallback)callback)); + } + + /** + * asks the voter whether to cast or audit the ballot + * @param command a simple CastOrAuditUICommand with the callback + */ + private void doCastOrAudit(CastOrAuditUICommand command) { + logger.debug("UI entered doCastOrAudit"); + System.out.println ("Finalizing your vote. Do you wish to (C)ast or (A)udit?"); + + FinalizeBallotChoice fChoice; + + try { + String s = readInputLine(); + if (s.equals("cast") || s.equals("c")) { + fChoice = FinalizeBallotChoice.CAST; + } + else if (s.equals("audit") || s.equals("a")) { + fChoice = FinalizeBallotChoice.AUDIT; + } + else { + throw new IllegalArgumentException("UI could not understand the answer for cast/audit question '" + s + "'"); + } + ControllerCallback callback = command.getCallback(); + assert (callback instanceof CastOrAuditCallback); + ((CastOrAuditCallback)callback).onSuccess(fChoice); + } + catch (IllegalArgumentException|IOException e) { + String errorMessage = "doCastOrAudit: some error with reading input from console. details: " + e; + logger.error(errorMessage); + command.getCallback().onFailure(e); + } + } + + + /** + * makes the UI (and voter) wait for something else to happen, by registering a WaitForFinishUICommand in the queue + * @param message a message to show the user on the UI device while waiting + * @param callback a success return value of the wait (cancelling returns false) + */ + @Override + public void notifyVoterToWaitForFinish(UIElement message, FutureCallback callback) { + logger.debug("UI interface call to notifyVoterToWaitForFinish"); + cmdPend.trample(new WaitForFinishUICommand(message, (WaitForFinishCallback)callback)); + } + + /** + * Tells the voter (in the console) to wait until some other process is finished + * @param command a simple WaitForFinishUICommand with the callback + */ + public void doWaitForFinish (WaitForFinishUICommand command) { + logger.debug("UI entered doWaitForFinish"); + + startWaitingTime = new Date(); + + UIElement message = command.getMessage(); + String messageString; + if (message.getType() != UIElementDataType.TEXT) { + messageString = "Default message: encountered an error. System halting"; + } else { + messageString = bytesToString(message.getData()); + } + System.out.println(messageString); + System.out.print ("Waiting : ."); + } + + /** + * show an error to the voter. Halts the system until a technician handles it + * @param errorMessage message to show in UI device + * @param callback returns interrupt + */ + @Override + public void showErrorMessageAndHalt(UIElement errorMessage, FutureCallback callback) { + logger.debug("UI interface call to showErrorMessageAndHalt"); + throw new UnsupportedOperationException("Not implemented becuase currently not sure if we ever use it."); + } + + /** + * show an error to the voter. let him press a (chosen) button for handling the error. + * @param errorMessage message to show in UI device + * @param buttonLabels labels for buttons to present to voter + * @param callback the number of the selected button + */ + @Override + public void showErrorMessageWithButtons(UIElement errorMessage, UIElement[] buttonLabels, FutureCallback callback) { + logger.debug("UI interface call to showErrorMessageWithButtons"); + cmdPend.trample(new FatalErrorUICommand(errorMessage, buttonLabels, (ControllerCallback)callback)); + } + + /** + * show an error to the voter. let him press a (chosen) button for handling the error. + * @param command a FatalErrorUICommand with the callback + */ + private void doFatalError (FatalErrorUICommand command) { + logger.debug("UI entered doFatalError"); + + UIElement errorMessage = command.getErrorMessage(); + String errorMessageString; + if (errorMessage.getType() != UIElementDataType.TEXT) { + errorMessageString = "Default message: encountered an error. System halting"; + } else { + errorMessageString = bytesToString(errorMessage.getData()); + } + + UIElement[] buttonLabels = command.getButtonLabels(); + String[] buttonLabelStrings = new String[buttonLabels.length]; + for (int i = 0; i < buttonLabels.length; ++i) { + if (buttonLabels[i].getType() != UIElementDataType.TEXT) { + buttonLabelStrings[i] = ""; + } else { + buttonLabelStrings[i] = bytesToString(errorMessage.getData()); + } + } + + System.out.println(errorMessageString); + for (int i = 0; i < buttonLabelStrings.length; ++i) { + System.out.println("" + i + " - " + buttonLabelStrings[i]); + } + + try { + String s = readInputLine(); + Integer chosenButton = new Integer(s); + command.getCallback().onSuccess(chosenButton); + } + catch (IOException e) { + String err = "doFatalError: some error with reading input from console. details: " + e; + logger.error(err); + command.getCallback().onFailure(e); + } + + } + + + /** + * this method is run when a TickCommand was received while in waiting state + */ + private void doTick () { + if (startWaitingTime != null) { + System.out.print ("."); // still waiting + } + } + + /** + * get an input line from the console + * @return a line from the voter + * @throws IOException + */ + private String readInputLine() throws IOException{ + String s; + try { + s = this.bufferedReader.readLine(); + if (null == s) { + throw new IOException(); + } + } catch (IOException e) { + String errorMessage = "readInputLine: some error with reading input from console. details: " + e; + logger.error(errorMessage); + throw new IOException(e); + } + return s; + } + + + /** + * asserts that the question data matches the types that we can handle in the ConsoleUI + * (better and more sophisticated UI will be able to handle more types of data) + * @param questions list of the questions + */ + private void assertQuestionsAreValid (List questions) { + for (int index = 0; index < questions.size(); ++index) { + BallotQuestion question = questions.get(index); + if (question.getIsMandatory()) { + String errorMessage = "askVoterQuestions: question number " + index + " is marked as mandatory"; + logger.error(errorMessage); + throw new UnsupportedOperationException(errorMessage); + } + if (!isQuestionOnlyText(question)) { + String errorMessage = "askVoterQuestions: question number " + index + " is not only text"; + logger.error(errorMessage); + throw new UnsupportedOperationException(errorMessage); + } + } + } + + + /** + * present the questions to the voter console sequentially. + * Voter may choose at any time to skip a question, go back or even cancel the whole session + * @param questions list of questions to present + * @return list of answers to the questions (at the same order) + * @throws VoterCancelThrowable this is thrown if a voter chose to cancel in the middle of the process + * @throws IOException + */ + private List askVoterForAnswers(List questions) throws VoterCancelThrowable, IOException { + + assertQuestionsAreValid (questions); + + List answers = new ArrayList<>(); + int index = 0; + while (index < questions.size()) { + BallotQuestion question = questions.get(index); + System.out.println("Question number " + index); + showQuestionInConsole(question); + + System.out.println("UI screen: Enter your answer. You can also type '(b)ack' or '(c)ancel' or '(s)kip"); + String s = readInputLine(); + + if ((s.equals("cancel") || s.equals("c")) || (index == 0 && (s.equals("back") || s.equals("b")))) { + throw new VoterCancelThrowable(); + } + else if (s.equals("back") || s.equals("b")) { + --index; + answers.remove(index); + } + else if (s.equals("skip") || s.equals("s")) { + answers.add(translateStringAnswerToProtoBufMessageAnswer("")); + ++index; + } + else { + answers.add(translateStringAnswerToProtoBufMessageAnswer(s)); + ++index; + } + } + return answers; + } + + /** + * present a question in the console to the voter + * @param question a text ballot question + */ + private void showQuestionInConsole(BallotQuestion question) { + if (!isQuestionOnlyText(question)) { + System.err.println("debug: an element in question is not of TEXT type"); + throw new UnsupportedOperationException(); + } + + System.out.println("Question text: " + bytesToString(question.getQuestion().getData())); + System.out.println("Description: " + bytesToString(question.getDescription().getData())); + int answerIndex = 0; + for (UIElement answer : question.getAnswerList()) { + ++answerIndex; + System.out.println("Answer " + answerIndex + ": " + bytesToString(answer.getData())); + } + } + + /** + * checks whether the data of the question is only text. This is the only type we can handle in ConsoleUI + * @param question a ballot question to check + * @return True if the question data is only text + */ + private boolean isQuestionOnlyText (BallotQuestion question) { + boolean isText = true; + if (question.getQuestion().getType() != UIElementDataType.TEXT + || question.getDescription().getType() != UIElementDataType.TEXT) { + isText = false; + } + for (UIElement answer : question.getAnswerList()) { + if (answer.getType() != UIElementDataType.TEXT) { + isText = false; + } + } + return isText; + } + + + private BallotAnswer translateStringAnswerToProtoBufMessageAnswer(String s) { + BallotAnswer.Builder bab = BallotAnswer.newBuilder(); + StringTokenizer st = new StringTokenizer(s); + while (st.hasMoreTokens()) { + bab.addAnswer(Integer.parseInt(st.nextToken())); + } + return bab.build(); + } + + /* + * Returns the UTF8 decoding of byte-string data + */ + private static String bytesToString(ByteString data) { + return data.toStringUtf8(); + } + + + private boolean wasShutDownCalled () { + return shutDownHasBeenCalled; + } + +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/TickerTimerTask.java b/voting-booth/src/main/java/meerkat/voting/ui/TickerTimerTask.java new file mode 100644 index 0000000..42924c2 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/TickerTimerTask.java @@ -0,0 +1,21 @@ +package meerkat.voting.ui; + +import meerkat.voting.ui.uicommands.TickCommand; +import meerkat.voting.ui.uicommands.UICommand; + +import java.util.TimerTask; + +/** + * A thread that sends the UI clock TickCommands in a given constant frequency + */ +class TickerTimerTask extends TimerTask { + private CommandPend cmdPend; + public TickerTimerTask (CommandPend cmdPend) { + this.cmdPend = cmdPend; + } + + @Override + public void run() { + cmdPend.offer(new TickCommand(null)); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/VotingBoothUI.java b/voting-booth/src/main/java/meerkat/voting/ui/VotingBoothUI.java new file mode 100644 index 0000000..c4e1f1e --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/VotingBoothUI.java @@ -0,0 +1,82 @@ +package meerkat.voting.ui; + +import com.google.common.util.concurrent.FutureCallback; +import meerkat.protobuf.Voting.*; + +import java.util.List; + + +/** + * An interface for the UI component of the voting booth + */ +public interface VotingBoothUI { + + /** + * a simple enum for the voter's finalize choice + */ + public enum FinalizeBallotChoice { + CAST, + AUDIT + } + + /** + * shut down the UI + */ + public void callShutDown(); + + /** + * Starts a new session for a voter. Presents whatever initial info is decided to show at the beginning + * @param callback - a boolean future callback to return when done + */ + public void startNewVoterSession (FutureCallback callback); + + /** + * Present a question to the voter to decide on his voting channel. + * @param questions questions to determine the right voting channel for this voter + * @param callback that's where we store the answers to decide channel upon for the current voter + */ + public void chooseChannel (List questions, FutureCallback> callback); + + /** + * Presents the set of questions to the voter. Collect all his responses. + * @param questions all ballot questions to present to the voter + * @param callback the responses to the questions collected by the UI, to send back to the controller. Responses are null if voter chose to cancel session + */ + public void askVoterQuestions (List questions, FutureCallback> callback); + + /** + * Get a response from the voter on how to finalize the ballot. + * @param callback the returned choice of how to finalize the ballot + */ + public void castOrAudit (FutureCallback callback); + + + + // Admin scenario methods + //TODO: the admin scenario still needs some more thinking + + + /** + * present a wait-for-finish screen to the voter + * @param message a message to show the user on the UI device while waiting + * @param callback a success return value of the wait (cancelling returns false) + */ + public void notifyVoterToWaitForFinish (UIElement message, FutureCallback callback); + + /** + * show a fatal error message in the UI. Halts system. Waits for administrator interrupt or reset + * @param errorMessage message to show in UI device + * @param callback returns interrupt + */ + public void showErrorMessageAndHalt (UIElement errorMessage, FutureCallback callback); + + /** + * show an error message and let user press his chosen button for continuation + * @param errorMessage message to show in UI device + * @param buttonLabels labels for buttons to present to voter + * @param callback the number of the selected button + */ + public void showErrorMessageWithButtons (UIElement errorMessage, UIElement[] buttonLabels, + FutureCallback callback); + +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/uicommands/CastOrAuditUICommand.java b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/CastOrAuditUICommand.java new file mode 100644 index 0000000..583a4a2 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/CastOrAuditUICommand.java @@ -0,0 +1,13 @@ +package meerkat.voting.ui.uicommands; + +import meerkat.voting.controller.callbacks.*; +import meerkat.voting.ui.VotingBoothUI.FinalizeBallotChoice; + +/** + * This command signals the UI that the voter should now choose whether to Cast or Audit the ballot + */ +public class CastOrAuditUICommand extends UICommand { + public CastOrAuditUICommand(ControllerCallback callback) { + super(callback); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/uicommands/ChannelChoiceUICommand.java b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/ChannelChoiceUICommand.java new file mode 100644 index 0000000..185e255 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/ChannelChoiceUICommand.java @@ -0,0 +1,24 @@ +package meerkat.voting.ui.uicommands; + +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.callbacks.*; + +import java.util.List; + +/** + * This command signals the UI to present channel choice questions to the voter and send back the answers + */ +public class ChannelChoiceUICommand extends UICommand> { + + private final List questions; + + public ChannelChoiceUICommand(List questions, ControllerCallback> callback) + { + super(callback); + this.questions = questions; + } + + public List getQuestions () { + return this.questions; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/uicommands/FatalErrorUICommand.java b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/FatalErrorUICommand.java new file mode 100644 index 0000000..98af0e4 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/FatalErrorUICommand.java @@ -0,0 +1,28 @@ +package meerkat.voting.ui.uicommands; + +import meerkat.protobuf.Voting.UIElement; +import meerkat.voting.controller.callbacks.*; + +/** + * This command signals the UI that a fatal error occurred and it should notify the voter + */ +public class FatalErrorUICommand extends UICommand { + + private final UIElement errorMessage; + private final UIElement[] buttonLabels; + + public FatalErrorUICommand(UIElement errorMessage, UIElement[] buttonLabels, ControllerCallback callback) + { + super(callback); + this.errorMessage = errorMessage; + this.buttonLabels = buttonLabels; + } + + public UIElement getErrorMessage() { + return errorMessage; + } + + public UIElement[] getButtonLabels() { + return buttonLabels; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/uicommands/RaceVotingUICommand.java b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/RaceVotingUICommand.java new file mode 100644 index 0000000..9b329bc --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/RaceVotingUICommand.java @@ -0,0 +1,24 @@ +package meerkat.voting.ui.uicommands; + +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.callbacks.*; + +import java.util.List; + +/** + * This command signals the UI to present the race voting questions to the voter + */ +public class RaceVotingUICommand extends UICommand> { + + private final List questions; + + public RaceVotingUICommand(List questions, ControllerCallback> callback) + { + super(callback); + this.questions = questions; + } + + public List getQuestions () { + return this.questions; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/uicommands/StartSessionUICommand.java b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/StartSessionUICommand.java new file mode 100644 index 0000000..b40bb5d --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/StartSessionUICommand.java @@ -0,0 +1,13 @@ +package meerkat.voting.ui.uicommands; + +import meerkat.voting.controller.callbacks.*; + +/** + * This command signals the UI to present a new session to a voter + */ +public class StartSessionUICommand extends UICommand { + + public StartSessionUICommand(ControllerCallback callback) { + super(callback); + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/uicommands/TickCommand.java b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/TickCommand.java new file mode 100644 index 0000000..e9bb2b7 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/TickCommand.java @@ -0,0 +1,15 @@ +package meerkat.voting.ui.uicommands; + +import meerkat.voting.controller.callbacks.*; + +/** + * This is a special UI command which just points out that a tick of the clock occurred + * (so a progress bar can advance while waiting) + */ +public class TickCommand extends UICommand { + + public TickCommand(ControllerCallback callback) { + super(callback); + assert null == callback; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/uicommands/UICommand.java b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/UICommand.java new file mode 100644 index 0000000..e579133 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/UICommand.java @@ -0,0 +1,18 @@ +package meerkat.voting.ui.uicommands; + +import meerkat.voting.controller.callbacks.*; + +/** + * Base class for the commands to put in the UI queue + */ +public abstract class UICommand { + protected final ControllerCallback callback; + + protected UICommand(ControllerCallback callback) { + this.callback = callback; + } + + public ControllerCallback getCallback () { + return this.callback; + } +} diff --git a/voting-booth/src/main/java/meerkat/voting/ui/uicommands/WaitForFinishUICommand.java b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/WaitForFinishUICommand.java new file mode 100644 index 0000000..5e5d504 --- /dev/null +++ b/voting-booth/src/main/java/meerkat/voting/ui/uicommands/WaitForFinishUICommand.java @@ -0,0 +1,22 @@ +package meerkat.voting.ui.uicommands; + +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.callbacks.*; + +/** + * This command signals the UI to wait with an appropriate message until a new command replaces this state + */ +public class WaitForFinishUICommand extends UICommand { + + private final UIElement message; + + public WaitForFinishUICommand(UIElement message, ControllerCallback callback) + { + super(callback); + this.message = message; + } + + public UIElement getMessage () { + return this.message; + } +} diff --git a/voting-booth/src/test/java/meerkat/voting/NetworkVirtualPrinterTest.java b/voting-booth/src/test/java/meerkat/voting/NetworkVirtualPrinterTest.java new file mode 100644 index 0000000..80c9b72 --- /dev/null +++ b/voting-booth/src/test/java/meerkat/voting/NetworkVirtualPrinterTest.java @@ -0,0 +1,240 @@ +package meerkat.voting; + + +import com.google.common.util.concurrent.FutureCallback; + +import meerkat.protobuf.Crypto.*; +import meerkat.protobuf.PollingStation.*; + +import com.google.protobuf.ByteString; +import meerkat.pollingstation.PollingStationScanner; +import meerkat.pollingstation.PollingStationWebScanner; + +import meerkat.protobuf.Voting.*; +import meerkat.voting.controller.callbacks.OutputDeviceCommitCallback; +import meerkat.voting.controller.callbacks.OutputDeviceFinalizeCallback; +import meerkat.voting.output.NetworkVirtualPrinter; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.Semaphore; + + +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * A test for the NetworkVirtualPrinter + * coded with too much effort by Hai, based on the PollingStationWebScannerTest coded by Arbel + */ + + + +public class NetworkVirtualPrinterTest { + + private PollingStationScanner.Consumer scanner; + private static final String ADDRESS = "http://localhost"; + private static final String SUB_ADDRESS = ""; + private static final int PORT = 8080; + + private Semaphore semaphore0; + private Semaphore semaphore1; + private Semaphore semaphore2; + private Throwable thrown; + private boolean dataIsAsExpected; + + private NetworkVirtualPrinter networkPrinter; + + + + + + private class ScanHandler implements FutureCallback { + + private final ScannedData expectedData; + + public ScanHandler(ScannedData expectedData) { + this.expectedData = expectedData; + } + + @Override + public void onSuccess(ScannedData result) { + dataIsAsExpected = result.equals(expectedData); + semaphore2.release(); + } + + @Override + public void onFailure(Throwable t) { + dataIsAsExpected = false; + thrown = t; + semaphore2.release(); + } + } + + private class CommitHandler extends OutputDeviceCommitCallback { + + private boolean success; + public String errorMessage; + + public CommitHandler(int requestId, long serialNumber) { + super(requestId, serialNumber, null, null); + errorMessage = null; + success = false; + } + + @Override + public void onSuccess(Void v) { + System.out.println("CommitHandler success"); + success = true; + semaphore0.release(); + } + + @Override + public void onFailure(Throwable t) { + errorMessage = "Commit to ballot failed " + t.getMessage(); + semaphore0.release(); + } + + public boolean gotSuccess() { + return success; + } + } + + private class CastHandler extends OutputDeviceFinalizeCallback { + + private boolean success; + public String errorMessage; + + public CastHandler(int requestId, long serialNumber) { + super(requestId, serialNumber, null, null); + errorMessage = null; + success = false; + } + + @Override + public void onSuccess(Void v) { + System.out.println("CastHandler success"); + success = true; + semaphore1.release(); + } + + @Override + public void onFailure(Throwable t) { + errorMessage = "Cast to ballot failed " + t.getMessage(); + semaphore1.release(); + } + + public boolean gotSuccess() { + return success; + } + } + + @Before + public void init() { + + System.err.println("Setting up Scanner WebApp!"); + + scanner = new PollingStationWebScanner(PORT, SUB_ADDRESS); + + semaphore0 = new Semaphore(0); + semaphore1 = new Semaphore(0); + semaphore2 = new Semaphore(0); + thrown = null; + + try { + scanner.start(); + } catch (Exception e) { + assertThat("Could not start server: " + e.getMessage(), false); + } + + + networkPrinter = new NetworkVirtualPrinter(ADDRESS + ":" + PORT); + Thread outputThread = new Thread(networkPrinter); + outputThread.setName("Meerkat VB-Output Thread"); + outputThread.start(); + } + + @Test + public void testSuccessfulScan() throws InterruptedException { + + // create scannedData + + byte[] channel = {(byte) 1, (byte) 2}; + byte[] encMessageData = {(byte) 50, (byte) 51, (byte) 52}; + byte[] signatureData = {(byte) 93, (byte) 95, (byte) 95}; + byte[] signerId = {(byte) 17, (byte) 18, (byte) 19}; + int serialNumber = 17; + + PlaintextBallot plaintextBallot = PlaintextBallot.newBuilder() + .setChannelIdentifier(ByteString.copyFrom(channel)) + .setSerialNumber(serialNumber) + .build(); + + RerandomizableEncryptedMessage encMessage = RerandomizableEncryptedMessage.newBuilder() + .setData(ByteString.copyFrom(encMessageData)) + .build(); + + EncryptedBallot encryptedBallot = EncryptedBallot.newBuilder() + .setSerialNumber(serialNumber) + .setData(encMessage) + .build(); + + Signature signature = Signature.newBuilder() + .setType(SignatureType.ECDSA) + .setData(ByteString.copyFrom(signatureData)) + .setSignerId(ByteString.copyFrom(signerId)) + .build(); + + SignedEncryptedBallot signedEncryptedBallot = SignedEncryptedBallot.newBuilder() + .setEncryptedBallot(encryptedBallot) + .setSignature(signature) + .build(); + + ScannedData scannedData = ScannedData.newBuilder() + .setChannel(ByteString.copyFrom(channel)) + .setSignedEncryptedBallot(signedEncryptedBallot) + .build(); + + + scanner.subscribe(new ScanHandler(scannedData)); + + //Send scan + + CommitHandler commitHandler = new CommitHandler(0, serialNumber); + networkPrinter.commitToBallot(plaintextBallot, signedEncryptedBallot, commitHandler); + + semaphore0.acquire(); + + CastHandler castHandler = new CastHandler(1, serialNumber); + networkPrinter.castBallot(castHandler); + + semaphore1.acquire(); + + semaphore2.acquire(); + + // Make sure the response was valid + + assertThat("Commit to ballot callback did not receive success", commitHandler.gotSuccess()); + assertThat("Cast ballot callback did not receive success", castHandler.gotSuccess()); + + assertThat("Scanner has thrown an error", thrown == null); + assertThat("Scanned data received was incorrect", dataIsAsExpected); + + } + + @After + public void close() { + + System.err.println("Scanner WebApp shutting down..."); + + try { + scanner.stop(); + } catch (Exception e) { + assertThat("Could not stop server: " + e.getMessage(), false); + } + + networkPrinter.callShutDown(); + + } + +} \ No newline at end of file