350 lines
12 KiB
Java
350 lines
12 KiB
Java
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.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.concurrent.LinkedBlockingQueue;
|
|
|
|
/**
|
|
* Created by hai on 28/03/16.
|
|
*/
|
|
public class VotingBoothImpl implements VotingBoothController {
|
|
|
|
private BallotOutputDevice outputDevice;
|
|
private VBCryptoManager crypto;
|
|
private VotingBoothUI ui;
|
|
private StorageManager storageManager;
|
|
private List<BallotQuestion> questionsForChoosingChannel;
|
|
private List<BallotQuestion> questions;
|
|
private QuestionSelector questionSelector;
|
|
|
|
private LinkedBlockingQueue<ControllerCommand> queue;
|
|
|
|
private Logger logger;
|
|
|
|
private ControllerState state;
|
|
private volatile boolean shutDownHasBeenCalled;
|
|
|
|
protected final int MAX_REQUEST_IDENTIFIER = 100000;
|
|
private static int requestCounter = 0;
|
|
|
|
|
|
public VotingBoothImpl () {
|
|
logger = LoggerFactory.getLogger(VotingBoothImpl.class);
|
|
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) {
|
|
logger.info("init is called");
|
|
this.outputDevice = outputDevice;
|
|
this.crypto = vbCrypto;
|
|
this.ui = vbUI;
|
|
this.storageManager = vbStorageManager;
|
|
}
|
|
|
|
@Override
|
|
public void setBallotChannelChoiceQuestions(List<BallotQuestion> questions) {
|
|
logger.info("setting questions");
|
|
this.questionsForChoosingChannel = questions;
|
|
}
|
|
|
|
@Override
|
|
public void setChannelQuestionSelector(QuestionSelector selector) {
|
|
this.questionSelector = selector;
|
|
}
|
|
|
|
@Override
|
|
public void setBallotRaceQuestions(List<BallotQuestion> questions) {
|
|
logger.info("setting questions");
|
|
this.questions = questions;
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
logger.info("run command has been called");
|
|
runVotingFlow();
|
|
doShutDown();
|
|
|
|
}
|
|
|
|
private void runVotingFlow () {
|
|
logger.info("entered the voting flow");
|
|
|
|
queue.add(new RestartVotingCommand(generateRequestIdentifier(), state.currentBallotSerialNumber));
|
|
|
|
while (! wasShutDownCalled()) {
|
|
try {
|
|
ControllerCommand task = queue.take();
|
|
handleSingleTask (task);
|
|
}
|
|
catch (InterruptedException e) {
|
|
logger.warn ("Interrupted while reading from task queue " + e);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void callShutDown() {
|
|
logger.info("callShutDown command has been called");
|
|
shutDownHasBeenCalled = true;
|
|
queue.clear();
|
|
ui.callShutDown();
|
|
outputDevice.callShutDown();
|
|
}
|
|
|
|
private void handleSingleTask (ControllerCommand task) {
|
|
if (task.getBallotSerialNumber() != state.currentBallotSerialNumber && !(task instanceof RestartVotingCommand)) {
|
|
// probably an old command relating to some old ballot serial number. Simply log it and ignore it.
|
|
String errorMessage = "handleSingleTask: received a task too old. " +
|
|
task.getBallotSerialNumber() + " " + state.currentBallotSerialNumber;
|
|
logger.debug(errorMessage);
|
|
return;
|
|
}
|
|
|
|
if (task instanceof RestartVotingCommand) {
|
|
doRestartVoting ();
|
|
}
|
|
else if (task instanceof ChannelChoiceCommand) {
|
|
doChooseChannel();
|
|
}
|
|
else if (task instanceof ChannelDeterminedCommand) {
|
|
doSetChannelAndAskQuestions ((ChannelDeterminedCommand)task);
|
|
}
|
|
else if (task instanceof ChooseFinalizeOptionCommand) {
|
|
doChooseFinalizeOption();
|
|
}
|
|
else if (task instanceof CastCommand) {
|
|
doFinalize(false);
|
|
}
|
|
else if (task instanceof AuditCommand) {
|
|
doFinalize(true);
|
|
}
|
|
else if (task instanceof EncryptAndCommitBallotCommand) {
|
|
doCommit ((EncryptAndCommitBallotCommand)task);
|
|
}
|
|
else if (task instanceof ReportErrorCommand) {
|
|
doReportErrorAndForceRestart((ReportErrorCommand)task);
|
|
}
|
|
else {
|
|
logger.error("handleSingleTask: unknown type of ControllerCommand received: " + task.getClass().getName());
|
|
doReportErrorAndForceRestart(SystemMessages.getSomethingWrongMessage());
|
|
}
|
|
}
|
|
|
|
private boolean wasShutDownCalled () {
|
|
return shutDownHasBeenCalled;
|
|
}
|
|
|
|
private void doShutDown () {
|
|
logger.info("running callShutDown");
|
|
state.clearPlaintext();
|
|
state.clearCiphertext();
|
|
state.stateIdentifier = VBState.SHUT_DOWN;
|
|
//TODO: add commands to actually shut down the machine
|
|
}
|
|
|
|
private void doRestartVoting () {
|
|
queue.clear();
|
|
state.clearPlaintext();
|
|
state.clearCiphertext();
|
|
state.stateIdentifier = VBState.NEW_VOTER;
|
|
state.currentBallotSerialNumber += 1;
|
|
|
|
ui.startNewVoterSession(new NewVoterCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue));
|
|
}
|
|
|
|
private void doReportErrorAndForceRestart(ReportErrorCommand task) {
|
|
doReportErrorAndForceRestart(task.getErrorMessage());
|
|
}
|
|
|
|
private void doReportErrorAndForceRestart(UIElement errorMessage) {
|
|
queue.clear();
|
|
state.clearPlaintext();
|
|
state.clearCiphertext();
|
|
state.stateIdentifier = VBState.FATAL_ERROR_FORCE_NEW_VOTER;
|
|
state.currentBallotSerialNumber += 1;
|
|
ui.showErrorMessageWithButtons(errorMessage,
|
|
new UIElement[]{SystemMessages.getRestartVotingButton()},
|
|
new ErrorMessageRestartCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue));
|
|
}
|
|
|
|
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));
|
|
}
|
|
else {
|
|
logger.debug("doChooseChannel: current state is " + state.stateIdentifier);
|
|
// ignore this request
|
|
}
|
|
}
|
|
|
|
private void doSetChannelAndAskQuestions (ChannelDeterminedCommand task) {
|
|
if (state.stateIdentifier == VBState.CHOOSE_CHANNEL) {
|
|
logger.debug("doing set channel and ask questions");
|
|
state.stateIdentifier = VBState.ANSWER_QUESTIONS;
|
|
List<BallotAnswer> channelChoiceAnswers = task.channelChoiceAnswers;
|
|
state.channelIdentifier = questionSelector.getChannelIdentifier(channelChoiceAnswers);
|
|
state.channelSpecificQuestions = questionSelector.selectQuestionsForVoter(state.channelIdentifier);
|
|
ui.askVoterQuestions(state.channelSpecificQuestions,
|
|
new VotingCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue));
|
|
}
|
|
else {
|
|
logger.debug("doSetChannelAndAskQuestions: current state is " + state.stateIdentifier);
|
|
// ignore this request
|
|
}
|
|
}
|
|
|
|
|
|
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));
|
|
}
|
|
else {
|
|
logger.debug("doChooseFinalizeOption: current state is " + state.stateIdentifier);
|
|
// ignore this request
|
|
}
|
|
}
|
|
private void doCommit (EncryptAndCommitBallotCommand task) {
|
|
if (state.stateIdentifier == VBState.ANSWER_QUESTIONS) {
|
|
logger.debug("doing commit");
|
|
state.stateIdentifier = VBState.COMMITTING_TO_BALLOT;
|
|
setBallotData (task);
|
|
ui.notifyVoterToWaitForFinish(SystemMessages.getWaitForCommitMessage(),
|
|
new WaitForFinishCallback(generateRequestIdentifier(),
|
|
state.currentBallotSerialNumber,
|
|
this.queue));
|
|
outputDevice.commitToBallot(state.plaintextBallot,
|
|
state.signedEncryptedBallot,
|
|
new OutputDeviceCommitCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue));
|
|
}
|
|
else {
|
|
logger.debug("doCommit: current state is " + state.stateIdentifier);
|
|
// ignore this request
|
|
}
|
|
}
|
|
|
|
private void setBallotData (EncryptAndCommitBallotCommand task) {
|
|
state.plaintextBallot = PlaintextBallot.newBuilder()
|
|
.setSerialNumber(task.getBallotSerialNumber())
|
|
.addAllAnswers(task.getVotingAnswers())
|
|
.build();
|
|
EncryptionAndSecrets encryptionAndSecrets = null;
|
|
try {
|
|
encryptionAndSecrets = crypto.encrypt(state.plaintextBallot);
|
|
}
|
|
catch (SignatureException | IOException e) {
|
|
// TODO: handle exception
|
|
}
|
|
state.signedEncryptedBallot = encryptionAndSecrets.getSignedEncryptedBallot();
|
|
state.secrets = encryptionAndSecrets.getSecrets();
|
|
}
|
|
|
|
private void doFinalize (boolean auditRequested) {
|
|
if (state.stateIdentifier == VBState.CAST_OR_AUDIT) {
|
|
logger.debug("finalizing");
|
|
state.stateIdentifier = VBState.FINALIZING;
|
|
if (auditRequested) {
|
|
ui.notifyVoterToWaitForFinish(SystemMessages.getWaitForAuditMessage(),
|
|
new WaitForFinishCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue));
|
|
outputDevice.audit(state.secrets,
|
|
new OutputDeviceFinalizeCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue));
|
|
}
|
|
else {
|
|
ui.notifyVoterToWaitForFinish(SystemMessages.getWaitForCastMessage(),
|
|
new WaitForFinishCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue));
|
|
outputDevice.castBallot(
|
|
new OutputDeviceFinalizeCallback(generateRequestIdentifier(), state.currentBallotSerialNumber, this.queue));
|
|
}
|
|
}
|
|
else {
|
|
logger.debug("doFinalize: current state is " + state.stateIdentifier);
|
|
// ignore this request
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private enum VBState {
|
|
NEW_VOTER,
|
|
CHOOSE_CHANNEL,
|
|
ANSWER_QUESTIONS,
|
|
COMMITTING_TO_BALLOT,
|
|
CAST_OR_AUDIT,
|
|
FINALIZING,
|
|
FATAL_ERROR_FORCE_NEW_VOTER,
|
|
SHUT_DOWN
|
|
}
|
|
|
|
|
|
private class ControllerState {
|
|
public VBState stateIdentifier;
|
|
public byte[] channelIdentifier;
|
|
public List<BallotQuestion> 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;
|
|
}
|
|
|
|
|
|
public void clearPlaintext () {
|
|
plaintextBallot = null;
|
|
}
|
|
|
|
public void clearCiphertext () {
|
|
signedEncryptedBallot = null;
|
|
secrets = null;
|
|
}
|
|
}
|
|
|
|
|
|
private int generateRequestIdentifier() {
|
|
++requestCounter;
|
|
if (requestCounter >= MAX_REQUEST_IDENTIFIER) {
|
|
requestCounter = 1;
|
|
}
|
|
return requestCounter;
|
|
}
|
|
|
|
|
|
}
|
|
|