Added the verefication process to the receiving of messages from voter registry
parent
16a3102ca4
commit
c8e747285c
Binary file not shown.
|
@ -0,0 +1,21 @@
|
||||||
|
package meerkat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the validation option for objects
|
||||||
|
* @param <T>
|
||||||
|
*/
|
||||||
|
public interface MessageValidator<T>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Throws ValidationError when no part of the object is valid else return the valid part
|
||||||
|
* @param object object which will be checked
|
||||||
|
* @return T object
|
||||||
|
*/
|
||||||
|
T validate(T object) throws ValidationError ;
|
||||||
|
|
||||||
|
class ValidationError extends Exception {
|
||||||
|
public ValidationError(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,18 @@
|
||||||
package meerkat.Registry;
|
package meerkat.Registry;
|
||||||
|
|
||||||
|
import meerkat.MessageValidator;
|
||||||
import meerkat.VoterRegistry;
|
import meerkat.VoterRegistry;
|
||||||
import meerkat.bulletinboard.BulletinBoardClient;
|
import meerkat.bulletinboard.BulletinBoardClient.ClientCallback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: add logging
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Vladimir Eliezer Tokarev on 2/19/2016.
|
* Created by Vladimir Eliezer Tokarev on 2/19/2016.
|
||||||
* Handles the the after post state of VoterRegistry methods (that uses bulletinBoardClient to communicate with the server)
|
* Handles the the after post state of VoterRegistry methods (that uses bulletinBoardClient to communicate with the server)
|
||||||
*/
|
*/
|
||||||
public class BooleanCallBack implements BulletinBoardClient.ClientCallback {
|
public class BooleanCallBack implements ClientCallback<Boolean>, MessageValidator<Boolean> {
|
||||||
public VoterRegistry.RegistryCallBack callback;
|
public VoterRegistry.RegistryCallBack callback;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,7 +28,7 @@ public class BooleanCallBack implements BulletinBoardClient.ClientCallback {
|
||||||
* @param msg the message that the bulletinBoardClient passes to the callback
|
* @param msg the message that the bulletinBoardClient passes to the callback
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void handleCallback(Object msg) {
|
public void handleCallback(Boolean msg) {
|
||||||
callback.HandleResult(msg);
|
callback.HandleResult(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,4 +40,15 @@ public class BooleanCallBack implements BulletinBoardClient.ClientCallback {
|
||||||
public void handleFailure(Throwable t) {
|
public void handleFailure(Throwable t) {
|
||||||
callback.HandleResult(false);
|
callback.HandleResult(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* always return true because there no need to check boolean
|
||||||
|
* @param object object which will be checked
|
||||||
|
* @return
|
||||||
|
* @throws ValidationError
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Boolean validate(Boolean object) throws ValidationError {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package meerkat.Registry;
|
package meerkat.Registry;
|
||||||
|
|
||||||
import meerkat.VoterRegistryMessage;
|
import meerkat.VoterRegistryMessage;
|
||||||
import meerkat.protobuf.BulletinBoardAPI;
|
import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage;
|
||||||
|
import meerkat.protobuf.BulletinBoardAPI.FilterType;
|
||||||
|
import meerkat.protobuf.BulletinBoardAPI.MessageFilter;
|
||||||
|
import meerkat.protobuf.BulletinBoardAPI.MessageFilterList;
|
||||||
|
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -10,7 +13,9 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: add logging to this utils
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Vladimir Eliezer Tokarev on 1/15/2016.
|
* Created by Vladimir Eliezer Tokarev on 1/15/2016.
|
||||||
|
@ -30,7 +35,7 @@ public abstract class CollectionMessagesUtils {
|
||||||
* @return List<VoterRegistryMessage>
|
* @return List<VoterRegistryMessage>
|
||||||
*/
|
*/
|
||||||
public static List<VoterRegistryMessage> ConvertToVoterRegistryMessages(
|
public static List<VoterRegistryMessage> ConvertToVoterRegistryMessages(
|
||||||
List<BulletinBoardAPI.BulletinBoardMessage> messages){
|
List<BulletinBoardMessage> messages){
|
||||||
return messages.stream().map(VoterRegistryMessage::new).collect(Collectors.toList());
|
return messages.stream().map(VoterRegistryMessage::new).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +49,8 @@ public abstract class CollectionMessagesUtils {
|
||||||
Map<String, VoterRegistryMessage> groupIdToMessage = new HashMap<>();
|
Map<String, VoterRegistryMessage> groupIdToMessage = new HashMap<>();
|
||||||
|
|
||||||
// iterate trough all the messages and put into the map the last updated groups actions
|
// iterate trough all the messages and put into the map the last updated groups actions
|
||||||
for (VoterRegistryMessage message : messages) {
|
for (int i = 0 ; i < messages.size() ; i++) {
|
||||||
|
VoterRegistryMessage message = messages.get(i);
|
||||||
String groupId = message.GetWantedTagFromBasicMessage(RegistryTags.GROUP_ID_TAG.toString());
|
String groupId = message.GetWantedTagFromBasicMessage(RegistryTags.GROUP_ID_TAG.toString());
|
||||||
VoterRegistryMessage temp = groupIdToMessage.get(groupId);
|
VoterRegistryMessage temp = groupIdToMessage.get(groupId);
|
||||||
|
|
||||||
|
@ -86,7 +92,8 @@ public abstract class CollectionMessagesUtils {
|
||||||
}
|
}
|
||||||
VoterRegistryMessage LatestMessage = messages.get(0);
|
VoterRegistryMessage LatestMessage = messages.get(0);
|
||||||
|
|
||||||
for (VoterRegistryMessage message : messages) {
|
for (int i = 0 ; i < messages.size() ; i++) {
|
||||||
|
VoterRegistryMessage message = messages.get(i);
|
||||||
if (message.GetBasicMessageActionTimestamp().before(LatestMessage.GetBasicMessageActionTimestamp())) {
|
if (message.GetBasicMessageActionTimestamp().before(LatestMessage.GetBasicMessageActionTimestamp())) {
|
||||||
LatestMessage = message;
|
LatestMessage = message;
|
||||||
}
|
}
|
||||||
|
@ -100,18 +107,16 @@ public abstract class CollectionMessagesUtils {
|
||||||
* @param tags the tags based on which the messages will be filtered
|
* @param tags the tags based on which the messages will be filtered
|
||||||
* @return MessageFilterList.
|
* @return MessageFilterList.
|
||||||
*/
|
*/
|
||||||
public static BulletinBoardAPI.MessageFilterList GetRelevantMessagesFilters(List<String> tags) {
|
public static MessageFilterList GenerateFiltersFromTags(List<String> tags) {
|
||||||
BulletinBoardAPI.MessageFilterList.Builder filters = BulletinBoardAPI.MessageFilterList.newBuilder();
|
MessageFilterList.Builder filters = MessageFilterList.newBuilder();
|
||||||
|
|
||||||
if (tags.isEmpty()){
|
if (tags.isEmpty()){
|
||||||
return filters.build();
|
return filters.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String tag : tags) {
|
for (int i = 0 ;i < tags.size() ; i++) {
|
||||||
BulletinBoardAPI.MessageFilter.Builder filter =
|
String tag = tags.get(i);
|
||||||
BulletinBoardAPI.MessageFilter.newBuilder().setTag(tag)
|
MessageFilter.Builder filter = MessageFilter.newBuilder().setTag(tag).setType(FilterType.TAG);
|
||||||
.setType(BulletinBoardAPI.FilterType.TAG);
|
|
||||||
|
|
||||||
filters.addFilter(filter);
|
filters.addFilter(filter);
|
||||||
}
|
}
|
||||||
return filters.build();
|
return filters.build();
|
||||||
|
|
|
@ -1,29 +1,54 @@
|
||||||
package meerkat.Registry;
|
package meerkat.Registry;
|
||||||
|
|
||||||
import meerkat.VoterRegistry;
|
import meerkat.MessageValidator;
|
||||||
|
import meerkat.VoterRegistry.RegistryCallBack;
|
||||||
import meerkat.VoterRegistryMessage;
|
import meerkat.VoterRegistryMessage;
|
||||||
import meerkat.bulletinboard.BulletinBoardClient;
|
import meerkat.bulletinboard.BulletinBoardClient.ClientCallback;
|
||||||
|
import meerkat.crypto.DigitalSignature;
|
||||||
import meerkat.protobuf.BulletinBoardAPI;
|
import meerkat.protobuf.BulletinBoardAPI;
|
||||||
|
import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage;
|
||||||
|
import meerkat.protobuf.Crypto;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static meerkat.Registry.CollectionMessagesUtils.*;
|
import static meerkat.Registry.CollectionMessagesUtils.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO : add logging
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Vladimir Eliezer Tokarev on 2/19/2016.
|
* Created by Vladimir Eliezer Tokarev on 2/19/2016.
|
||||||
* Object that handles the GetGroups or the GetPersonIDDetails of the simpleRegistry
|
* Object that handles the GetGroups or the GetPersonIDDetails of the simpleRegistry
|
||||||
*/
|
*/
|
||||||
public class RelevantDataCallBack implements BulletinBoardClient.ClientCallback<List<BulletinBoardAPI.BulletinBoardMessage>> {
|
public class RelevantDataCallBack implements ClientCallback<List<BulletinBoardMessage>>, MessageValidator<List<BulletinBoardMessage>> {
|
||||||
public VoterRegistry.RegistryCallBack callback;
|
public RegistryCallBack callback;
|
||||||
|
protected DigitalSignature validator;
|
||||||
|
protected Crypto.Signature signature;
|
||||||
|
protected InputStream certificateStream;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Init BooleanCallBack
|
* Init BooleanCallBack
|
||||||
* @param callback voter registry callback object
|
* @param callback voter registry callback object
|
||||||
|
* @param validator DigitalSignature object
|
||||||
|
* @param signature Crypto.Signature object
|
||||||
*/
|
*/
|
||||||
public RelevantDataCallBack(VoterRegistry.RegistryCallBack callback) {
|
public RelevantDataCallBack(RegistryCallBack callback,
|
||||||
|
DigitalSignature validator,
|
||||||
|
Crypto.Signature signature,
|
||||||
|
InputStream certificateStream) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
|
this.validator = validator;
|
||||||
|
this.signature = signature;
|
||||||
|
this.certificateStream = certificateStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,8 +56,10 @@ public class RelevantDataCallBack implements BulletinBoardClient.ClientCallback<
|
||||||
* @param msg List<BulletinBoardAPI.BulletinBoardMessage>
|
* @param msg List<BulletinBoardAPI.BulletinBoardMessage>
|
||||||
* @return true if the messages are with GROUP_ID_TAG tags
|
* @return true if the messages are with GROUP_ID_TAG tags
|
||||||
*/
|
*/
|
||||||
private boolean isAddToGroupsList(List<BulletinBoardAPI.BulletinBoardMessage> msg) {
|
private boolean isAddToGroupsList(List<BulletinBoardMessage> msg) {
|
||||||
for (String tag : msg.get(0).getMsg().getTagList()) {
|
List<String> tags = msg.get(0).getMsg().getTagList();
|
||||||
|
for (int i = 0 ;i < tags.size(); i++) {
|
||||||
|
String tag = tags.get(i);
|
||||||
if(tag.contains(RegistryTags.GROUP_ID_TAG.toString()))
|
if(tag.contains(RegistryTags.GROUP_ID_TAG.toString()))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
@ -49,24 +76,20 @@ public class RelevantDataCallBack implements BulletinBoardClient.ClientCallback<
|
||||||
* @param msg List<BulletinBoardAPI.BulletinBoardMessage>
|
* @param msg List<BulletinBoardAPI.BulletinBoardMessage>
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void handleCallback(List<BulletinBoardAPI.BulletinBoardMessage> msg) {
|
public void handleCallback(List<BulletinBoardMessage> msg) {
|
||||||
List<VoterRegistryMessage> messages = ConvertToVoterRegistryMessages(msg);
|
|
||||||
|
|
||||||
if(isAddToGroupsList(msg)){
|
|
||||||
try {
|
try {
|
||||||
|
List<VoterRegistryMessage> messages = ConvertToVoterRegistryMessages(validate(msg));
|
||||||
|
if(isAddToGroupsList(msg)) {
|
||||||
Map<String, VoterRegistryMessage> map = GetLatestGroupsActions(messages);
|
Map<String, VoterRegistryMessage> map = GetLatestGroupsActions(messages);
|
||||||
List<String> groupsOfUser = GetListOfGroupIds(map);
|
List<String> groupsOfUser = GetListOfGroupIds(map);
|
||||||
callback.HandleResult(groupsOfUser);
|
callback.HandleResult(groupsOfUser);
|
||||||
} catch (ParseException e) {
|
}
|
||||||
|
else {
|
||||||
|
callback.HandleResult(GetLatestMessage(messages));
|
||||||
|
}
|
||||||
|
} catch (ValidationError | ParseException | EmptyListException validationError) {
|
||||||
callback.HandleResult(null);
|
callback.HandleResult(null);
|
||||||
}
|
validationError.printStackTrace();
|
||||||
}
|
|
||||||
try {
|
|
||||||
callback.HandleResult(CollectionMessagesUtils.GetLatestMessage(messages));
|
|
||||||
} catch (ParseException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (CollectionMessagesUtils.EmptyListException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,4 +101,42 @@ public class RelevantDataCallBack implements BulletinBoardClient.ClientCallback<
|
||||||
public void handleFailure(Throwable t) {
|
public void handleFailure(Throwable t) {
|
||||||
callback.HandleResult(null);
|
callback.HandleResult(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* verify that the message is valid
|
||||||
|
* @param message BulletinBoardMessage instance
|
||||||
|
* @return true if the message is valid else false
|
||||||
|
*/
|
||||||
|
private boolean VerifyMessage(BulletinBoardAPI.UnsignedBulletinBoardMessage message)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
validator.loadVerificationCertificates(certificateStream);
|
||||||
|
validator.initVerify(signature);
|
||||||
|
validator.updateContent(message);
|
||||||
|
return validator.verify();
|
||||||
|
} catch (CertificateException | InvalidKeyException | SignatureException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all the valid messages from the list of bulletin board messages
|
||||||
|
* @param object object which will be checked
|
||||||
|
* @return List<BulletinBoardMessage>
|
||||||
|
* @throws ValidationError
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<BulletinBoardMessage> validate(List<BulletinBoardMessage> object) throws ValidationError {
|
||||||
|
List<BulletinBoardMessage> verifiedMessages = new ArrayList<>();
|
||||||
|
for (int i = 0 ; i < object.size() ; i++)
|
||||||
|
{
|
||||||
|
BulletinBoardMessage message = object.get(i);
|
||||||
|
if(VerifyMessage(message.getMsg()))
|
||||||
|
{
|
||||||
|
verifiedMessages.add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return verifiedMessages;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage;
|
||||||
import meerkat.protobuf.BulletinBoardAPI.UnsignedBulletinBoardMessage;
|
import meerkat.protobuf.BulletinBoardAPI.UnsignedBulletinBoardMessage;
|
||||||
import meerkat.protobuf.Crypto;
|
import meerkat.protobuf.Crypto;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
import java.security.SignatureException;
|
import java.security.SignatureException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -17,12 +18,17 @@ import java.util.List;
|
||||||
*/
|
*/
|
||||||
public class SimpleRegistry implements VoterRegistry{
|
public class SimpleRegistry implements VoterRegistry{
|
||||||
|
|
||||||
protected DigitalSignature signatory;
|
protected DigitalSignature signer;
|
||||||
protected BulletinBoardClient bulletinBoardClient ;
|
protected BulletinBoardClient bulletinBoardClient ;
|
||||||
|
protected Crypto.Signature signature;
|
||||||
|
protected InputStream certificateStream;
|
||||||
|
|
||||||
public SimpleRegistry(DigitalSignature signatory, BulletinBoardClient communicator) {
|
public SimpleRegistry(DigitalSignature signer,
|
||||||
this.signatory = signatory;
|
BulletinBoardClient communicator,
|
||||||
|
InputStream certificateStream) {
|
||||||
|
this.signer = signer;
|
||||||
this.bulletinBoardClient = communicator;
|
this.bulletinBoardClient = communicator;
|
||||||
|
this.certificateStream = certificateStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,15 +40,11 @@ public class SimpleRegistry implements VoterRegistry{
|
||||||
*/
|
*/
|
||||||
private BulletinBoardMessage CreateBulletinBoardMessage(UnsignedBulletinBoardMessage basicMessage) {
|
private BulletinBoardMessage CreateBulletinBoardMessage(UnsignedBulletinBoardMessage basicMessage) {
|
||||||
try {
|
try {
|
||||||
BulletinBoardMessage.Builder bulletinBoardMessage =
|
BulletinBoardMessage.Builder bulletinBoardMessage = BulletinBoardMessage.newBuilder();
|
||||||
BulletinBoardMessage.newBuilder();
|
|
||||||
|
|
||||||
signatory.updateContent(basicMessage);
|
signer.updateContent(basicMessage);
|
||||||
|
signature = signer.sign();
|
||||||
Crypto.Signature signature = signatory.sign();
|
bulletinBoardMessage.setMsg(basicMessage).addSig(signature);
|
||||||
bulletinBoardMessage.addSig(signature);
|
|
||||||
|
|
||||||
bulletinBoardMessage.setMsg(basicMessage);
|
|
||||||
|
|
||||||
return bulletinBoardMessage.build();
|
return bulletinBoardMessage.build();
|
||||||
} catch (SignatureException e) {
|
} catch (SignatureException e) {
|
||||||
|
@ -98,8 +100,8 @@ public class SimpleRegistry implements VoterRegistry{
|
||||||
List<String> GroupsActionsTags = new ArrayList<String>() {{
|
List<String> GroupsActionsTags = new ArrayList<String>() {{
|
||||||
add(RegistryTags.GROUP_ID_TAG + groupID.getId());
|
add(RegistryTags.GROUP_ID_TAG + groupID.getId());
|
||||||
}};
|
}};
|
||||||
bulletinBoardClient.readMessages(CollectionMessagesUtils.GetRelevantMessagesFilters(GroupsActionsTags),
|
bulletinBoardClient.readMessages(CollectionMessagesUtils.GenerateFiltersFromTags(GroupsActionsTags),
|
||||||
new RelevantDataCallBack(callback));
|
new RelevantDataCallBack(callback, signer, signature, certificateStream));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void GetPersonIDDetails(RegistryMessages.VoterID voterID, RegistryCallBack callback) {
|
public void GetPersonIDDetails(RegistryMessages.VoterID voterID, RegistryCallBack callback) {
|
||||||
|
@ -107,7 +109,7 @@ public class SimpleRegistry implements VoterRegistry{
|
||||||
add(RegistryTags.ID_TAG + voterID.getId());
|
add(RegistryTags.ID_TAG + voterID.getId());
|
||||||
add(RegistryTags.VOTER_ENTRY_TAG.toString());
|
add(RegistryTags.VOTER_ENTRY_TAG.toString());
|
||||||
}};
|
}};
|
||||||
bulletinBoardClient.readMessages(CollectionMessagesUtils.GetRelevantMessagesFilters(GroupsActionsTags),
|
bulletinBoardClient.readMessages(CollectionMessagesUtils.GenerateFiltersFromTags(GroupsActionsTags),
|
||||||
new RelevantDataCallBack(callback));
|
new RelevantDataCallBack(callback, signer, signature, certificateStream));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ public interface VoterRegistry {
|
||||||
* This interface will handle the end of methods of RegistryInstance
|
* This interface will handle the end of methods of RegistryInstance
|
||||||
*/
|
*/
|
||||||
interface RegistryCallBack<T> {
|
interface RegistryCallBack<T> {
|
||||||
boolean ActionSucceed = false;
|
|
||||||
void HandleResult(T result);
|
void HandleResult(T result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
package meerkat;
|
package meerkat;
|
||||||
|
|
||||||
import meerkat.protobuf.BulletinBoardAPI;
|
|
||||||
import meerkat.Registry.AccurateTimestamp;
|
import meerkat.Registry.AccurateTimestamp;
|
||||||
import meerkat.Registry.RegistryTags;
|
import meerkat.Registry.RegistryTags;
|
||||||
|
import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage;
|
||||||
|
import meerkat.protobuf.BulletinBoardAPI.UnsignedBulletinBoardMessage;
|
||||||
|
|
||||||
import java.sql.Timestamp;
|
import java.sql.Timestamp;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: add logging
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Vladimir Eliezer Tokarev on 1/15.2016
|
* Created by Vladimir Eliezer Tokarev on 1/15.2016
|
||||||
|
@ -13,11 +20,11 @@ import java.text.ParseException;
|
||||||
*/
|
*/
|
||||||
public class VoterRegistryMessage {
|
public class VoterRegistryMessage {
|
||||||
|
|
||||||
public BulletinBoardAPI.UnsignedBulletinBoardMessage base;
|
public UnsignedBulletinBoardMessage base;
|
||||||
|
|
||||||
public VoterRegistryMessage(BulletinBoardAPI.BulletinBoardMessage message) {
|
public VoterRegistryMessage(BulletinBoardMessage message) {
|
||||||
BulletinBoardAPI.UnsignedBulletinBoardMessage.Builder unsignedBase =
|
UnsignedBulletinBoardMessage.Builder unsignedBase =
|
||||||
BulletinBoardAPI.UnsignedBulletinBoardMessage.newBuilder().addAllTag(message.getMsg().getTagList());
|
UnsignedBulletinBoardMessage.newBuilder().addAllTag(message.getMsg().getTagList());
|
||||||
unsignedBase.setData(message.getMsg().getData());
|
unsignedBase.setData(message.getMsg().getData());
|
||||||
base = unsignedBase.build();
|
base = unsignedBase.build();
|
||||||
}
|
}
|
||||||
|
@ -29,7 +36,9 @@ public class VoterRegistryMessage {
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public String GetWantedTagFromBasicMessage(String tagName) {
|
public String GetWantedTagFromBasicMessage(String tagName) {
|
||||||
for (String tag : base.getTagList()) {
|
List<String> tags = base.getTagList();
|
||||||
|
for (int i = 0 ; i < tags.size() ; i++) {
|
||||||
|
String tag = tags.get(i);
|
||||||
if (tag.contains(tagName)) {
|
if (tag.contains(tagName)) {
|
||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
|
@ -44,7 +53,9 @@ public class VoterRegistryMessage {
|
||||||
* @throws ParseException
|
* @throws ParseException
|
||||||
*/
|
*/
|
||||||
public Timestamp GetBasicMessageActionTimestamp() throws ParseException {
|
public Timestamp GetBasicMessageActionTimestamp() throws ParseException {
|
||||||
for (String tag : base.getTagList()) {
|
List<String> tags = base.getTagList();
|
||||||
|
for (int i = 0 ; i < tags.size() ; i++) {
|
||||||
|
String tag = tags.get(i);
|
||||||
if (tag.contains(RegistryTags.ACTION_TIMESTAMP_TAG.toString())) {
|
if (tag.contains(RegistryTags.ACTION_TIMESTAMP_TAG.toString())) {
|
||||||
String[] tagParts = tag.split(" ");
|
String[] tagParts = tag.split(" ");
|
||||||
|
|
||||||
|
@ -62,7 +73,9 @@ public class VoterRegistryMessage {
|
||||||
* @return true when ADD_TO_GROUP_TAG exist else false
|
* @return true when ADD_TO_GROUP_TAG exist else false
|
||||||
*/
|
*/
|
||||||
public boolean IsGroupAdding() {
|
public boolean IsGroupAdding() {
|
||||||
for (String tag : base.getTagList()) {
|
List<String> tags = base.getTagList();
|
||||||
|
for (int i = 0 ; i < tags.size() ; i++) {
|
||||||
|
String tag = tags.get(i);
|
||||||
if (tag.contains(RegistryTags.ADD_TO_GROUP_TAG.toString())) {
|
if (tag.contains(RegistryTags.ADD_TO_GROUP_TAG.toString())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import com.google.protobuf.InvalidProtocolBufferException;
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
import junit.framework.TestCase;
|
import junit.framework.TestCase;
|
||||||
|
import meerkat.Registry.CollectionMessagesUtils;
|
||||||
|
import meerkat.Registry.RegistryTags;
|
||||||
import meerkat.RegistryMessages;
|
import meerkat.RegistryMessages;
|
||||||
import meerkat.SimpleRegistry;
|
import meerkat.SimpleRegistry;
|
||||||
import meerkat.VoterRegistry;
|
import meerkat.VoterRegistry;
|
||||||
|
@ -7,10 +9,9 @@ import meerkat.VoterRegistryMessage;
|
||||||
import meerkat.bulletinboard.BulletinBoardClient;
|
import meerkat.bulletinboard.BulletinBoardClient;
|
||||||
import meerkat.bulletinboard.ThreadedBulletinBoardClient;
|
import meerkat.bulletinboard.ThreadedBulletinBoardClient;
|
||||||
import meerkat.crypto.concrete.ECDSASignature;
|
import meerkat.crypto.concrete.ECDSASignature;
|
||||||
import meerkat.protobuf.BulletinBoardAPI;
|
import meerkat.protobuf.BulletinBoardAPI.BulletinBoardMessage;
|
||||||
|
import meerkat.protobuf.BulletinBoardAPI.MessageFilterList;
|
||||||
import meerkat.protobuf.Voting;
|
import meerkat.protobuf.Voting;
|
||||||
import meerkat.Registry.CollectionMessagesUtils;
|
|
||||||
import meerkat.Registry.RegistryTags;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
@ -26,10 +27,9 @@ import static meerkat.Registry.CollectionMessagesUtils.ConvertToVoterRegistryMes
|
||||||
* TODO: add logs prints for the tests to be clear what they are
|
* TODO: add logs prints for the tests to be clear what they are
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by Vladimir Eliezer Tokarev on 1/16/2016.
|
* Created by Vladimir Eliezer Tokarev on 1/16/2016.
|
||||||
* Tests the Simple meerkat.Registry contents
|
* Tests the Simple Registry contents
|
||||||
* NOTE: for most of this tests to pass there should run BulletinBoardServer
|
* NOTE: for most of this tests to pass there should run BulletinBoardServer
|
||||||
* that should be reachable on BULLETIN_BOARD_SERVER_ADDRESS
|
* that should be reachable on BULLETIN_BOARD_SERVER_ADDRESS
|
||||||
*/
|
*/
|
||||||
|
@ -37,8 +37,11 @@ public class SimpleRegistryTest extends TestCase {
|
||||||
|
|
||||||
private ECDSASignature signer;
|
private ECDSASignature signer;
|
||||||
private BulletinBoardClient bulletinBoardClient;
|
private BulletinBoardClient bulletinBoardClient;
|
||||||
|
private InputStream certStream;
|
||||||
private SecureRandom random = new SecureRandom();
|
private SecureRandom random = new SecureRandom();
|
||||||
|
|
||||||
public static String KEYFILE_EXAMPLE = "/certs/enduser-certs/user1-key-with-password-secret.p12";
|
public static String KEYFILE_EXAMPLE = "/certs/enduser-certs/user1-key-with-password-secret.p12";
|
||||||
|
public static String CERT1_PEM_EXAMPLE = "/certs/enduser-certs/user1.crt";
|
||||||
public static String KEYFILE_PASSWORD = "secret";
|
public static String KEYFILE_PASSWORD = "secret";
|
||||||
Semaphore jobSemaphore;
|
Semaphore jobSemaphore;
|
||||||
|
|
||||||
|
@ -59,11 +62,11 @@ public class SimpleRegistryTest extends TestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DummyBulletinBoardCallBackHandler implements BulletinBoardClient.ClientCallback<List<BulletinBoardAPI.BulletinBoardMessage>> {
|
public class DummyBulletinBoardCallBackHandler implements BulletinBoardClient.ClientCallback<List<BulletinBoardMessage>> {
|
||||||
public List<BulletinBoardAPI.BulletinBoardMessage> messages;
|
public List<BulletinBoardMessage> messages;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleCallback(List<BulletinBoardAPI.BulletinBoardMessage> msg)
|
public void handleCallback(List<BulletinBoardMessage> msg)
|
||||||
{
|
{
|
||||||
messages = msg;
|
messages = msg;
|
||||||
jobSemaphore.release();
|
jobSemaphore.release();
|
||||||
|
@ -76,10 +79,11 @@ public class SimpleRegistryTest extends TestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void createCertificateStream()
|
||||||
|
{
|
||||||
|
this.certStream = getClass().getResourceAsStream(CERT1_PEM_EXAMPLE);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the communication object (the bulletinBoardClient)
|
|
||||||
*/
|
|
||||||
private void CommunicatorSetup() {
|
private void CommunicatorSetup() {
|
||||||
bulletinBoardClient = new ThreadedBulletinBoardClient();
|
bulletinBoardClient = new ThreadedBulletinBoardClient();
|
||||||
String BULLETIN_BOARD_SERVER_ADDRESS = "http://localhost:8081";
|
String BULLETIN_BOARD_SERVER_ADDRESS = "http://localhost:8081";
|
||||||
|
@ -89,9 +93,6 @@ public class SimpleRegistryTest extends TestCase {
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the signer object which is the ECDSASignature
|
|
||||||
*/
|
|
||||||
private void SetSigner(){
|
private void SetSigner(){
|
||||||
try {
|
try {
|
||||||
signer = new ECDSASignature();
|
signer = new ECDSASignature();
|
||||||
|
@ -100,6 +101,7 @@ public class SimpleRegistryTest extends TestCase {
|
||||||
|
|
||||||
KeyStore.Builder keyStore = signer.getPKCS12KeyStoreBuilder(keyStream, password);
|
KeyStore.Builder keyStore = signer.getPKCS12KeyStoreBuilder(keyStream, password);
|
||||||
signer.loadSigningCertificate(keyStore);
|
signer.loadSigningCertificate(keyStore);
|
||||||
|
|
||||||
keyStream.close();
|
keyStream.close();
|
||||||
}
|
}
|
||||||
catch (Exception e){
|
catch (Exception e){
|
||||||
|
@ -113,6 +115,7 @@ public class SimpleRegistryTest extends TestCase {
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
SetSigner();
|
SetSigner();
|
||||||
CommunicatorSetup();
|
CommunicatorSetup();
|
||||||
|
createCertificateStream();
|
||||||
jobSemaphore = new Semaphore(0);
|
jobSemaphore = new Semaphore(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +124,7 @@ public class SimpleRegistryTest extends TestCase {
|
||||||
*/
|
*/
|
||||||
public void testSimpleRegistryCreation() {
|
public void testSimpleRegistryCreation() {
|
||||||
try {
|
try {
|
||||||
new SimpleRegistry(signer, bulletinBoardClient);
|
new SimpleRegistry(signer, bulletinBoardClient, certStream);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
assert false : "While creating the SimpleRegistry exception have been thrown " + e;
|
assert false : "While creating the SimpleRegistry exception have been thrown " + e;
|
||||||
}
|
}
|
||||||
|
@ -137,10 +140,12 @@ public class SimpleRegistryTest extends TestCase {
|
||||||
{
|
{
|
||||||
int counter = 0 ;
|
int counter = 0 ;
|
||||||
|
|
||||||
for (VoterRegistryMessage message :messages) {
|
for (int i = 0 ; i < messages.size() ; i++) {
|
||||||
|
VoterRegistryMessage message = messages.get(i);
|
||||||
int wantedTagsCounter = 0;
|
int wantedTagsCounter = 0;
|
||||||
|
|
||||||
for (String tag : tags) {
|
for (int j = 0 ;j < tags.size() ; j++) {
|
||||||
|
String tag = tags.get(j);
|
||||||
if(message.GetWantedTagFromBasicMessage(tag)!=null){
|
if(message.GetWantedTagFromBasicMessage(tag)!=null){
|
||||||
wantedTagsCounter++;
|
wantedTagsCounter++;
|
||||||
}
|
}
|
||||||
|
@ -154,7 +159,6 @@ public class SimpleRegistryTest extends TestCase {
|
||||||
return counter;
|
return counter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test that add voter creates new correct bulletin board message and adds the voter
|
* Test that add voter creates new correct bulletin board message and adds the voter
|
||||||
*/
|
*/
|
||||||
|
@ -166,14 +170,14 @@ public class SimpleRegistryTest extends TestCase {
|
||||||
RegistryMessages.VoterInfo voterInfo = RegistryMessages.VoterInfo.newBuilder().
|
RegistryMessages.VoterInfo voterInfo = RegistryMessages.VoterInfo.newBuilder().
|
||||||
setId(RegistryMessages.VoterID.newBuilder().setId(id)).setInfo(data).build();
|
setId(RegistryMessages.VoterID.newBuilder().setId(id)).setInfo(data).build();
|
||||||
|
|
||||||
SimpleRegistry registry = new SimpleRegistry(signer, bulletinBoardClient);
|
SimpleRegistry registry = new SimpleRegistry(signer, bulletinBoardClient, certStream);
|
||||||
registry.AddVoter(voterInfo, handler);
|
registry.AddVoter(voterInfo, handler);
|
||||||
|
|
||||||
jobSemaphore.acquire();
|
jobSemaphore.acquire();
|
||||||
assertEquals(1, handler.counter );
|
assertEquals(1, handler.counter );
|
||||||
|
|
||||||
List<String> tags = new ArrayList<String>(){{ add(RegistryTags.VOTER_ENTRY_TAG.toString());}};
|
List<String> tags = new ArrayList<String>(){{ add(RegistryTags.VOTER_ENTRY_TAG.toString());}};
|
||||||
BulletinBoardAPI.MessageFilterList filters = CollectionMessagesUtils.GetRelevantMessagesFilters(tags);
|
MessageFilterList filters = CollectionMessagesUtils.GenerateFiltersFromTags(tags);
|
||||||
DummyBulletinBoardCallBackHandler bulletinHandler = new DummyBulletinBoardCallBackHandler();
|
DummyBulletinBoardCallBackHandler bulletinHandler = new DummyBulletinBoardCallBackHandler();
|
||||||
bulletinBoardClient.readMessages(filters, bulletinHandler);
|
bulletinBoardClient.readMessages(filters, bulletinHandler);
|
||||||
|
|
||||||
|
@ -194,14 +198,14 @@ public class SimpleRegistryTest extends TestCase {
|
||||||
String id = new BigInteger(130, random).toString(32);
|
String id = new BigInteger(130, random).toString(32);
|
||||||
RegistryMessages.VoterID voterInfo = RegistryMessages.VoterID.newBuilder().setId(id).build();
|
RegistryMessages.VoterID voterInfo = RegistryMessages.VoterID.newBuilder().setId(id).build();
|
||||||
|
|
||||||
SimpleRegistry registry = new SimpleRegistry(signer, bulletinBoardClient);
|
SimpleRegistry registry = new SimpleRegistry(signer, bulletinBoardClient, certStream);
|
||||||
registry.SetVoted(voterInfo, handler);
|
registry.SetVoted(voterInfo, handler);
|
||||||
|
|
||||||
jobSemaphore.acquire();
|
jobSemaphore.acquire();
|
||||||
assertEquals(1, handler.counter );
|
assertEquals(1, handler.counter );
|
||||||
|
|
||||||
List<String> tags = new ArrayList<String>(){{ add(RegistryTags.VOTE_ACTION_TAG.toString());}};
|
List<String> tags = new ArrayList<String>(){{ add(RegistryTags.VOTE_ACTION_TAG.toString());}};
|
||||||
BulletinBoardAPI.MessageFilterList filters = CollectionMessagesUtils.GetRelevantMessagesFilters(tags);
|
MessageFilterList filters = CollectionMessagesUtils.GenerateFiltersFromTags(tags);
|
||||||
DummyBulletinBoardCallBackHandler bulletinHandler = new DummyBulletinBoardCallBackHandler();
|
DummyBulletinBoardCallBackHandler bulletinHandler = new DummyBulletinBoardCallBackHandler();
|
||||||
bulletinBoardClient.readMessages(filters, bulletinHandler);
|
bulletinBoardClient.readMessages(filters, bulletinHandler);
|
||||||
|
|
||||||
|
@ -224,7 +228,7 @@ public class SimpleRegistryTest extends TestCase {
|
||||||
.setVoterId(RegistryMessages.VoterID.newBuilder().setId(voterId))
|
.setVoterId(RegistryMessages.VoterID.newBuilder().setId(voterId))
|
||||||
.setGroupId(RegistryMessages.GroupID.newBuilder().setId(groupId)).build();
|
.setGroupId(RegistryMessages.GroupID.newBuilder().setId(groupId)).build();
|
||||||
|
|
||||||
SimpleRegistry registry = new SimpleRegistry(signer, bulletinBoardClient);
|
SimpleRegistry registry = new SimpleRegistry(signer, bulletinBoardClient, certStream);
|
||||||
registry.AddToGroup(voterInfo, handler);
|
registry.AddToGroup(voterInfo, handler);
|
||||||
|
|
||||||
jobSemaphore.acquire();
|
jobSemaphore.acquire();
|
||||||
|
@ -232,7 +236,7 @@ public class SimpleRegistryTest extends TestCase {
|
||||||
|
|
||||||
List<String> tags = new ArrayList<String>(){{ add(RegistryTags.GROUP_ACTION_TAG .toString()
|
List<String> tags = new ArrayList<String>(){{ add(RegistryTags.GROUP_ACTION_TAG .toString()
|
||||||
+ RegistryTags.ADD_TO_GROUP_TAG.toString());}};
|
+ RegistryTags.ADD_TO_GROUP_TAG.toString());}};
|
||||||
BulletinBoardAPI.MessageFilterList filters = CollectionMessagesUtils.GetRelevantMessagesFilters(tags);
|
MessageFilterList filters = CollectionMessagesUtils.GenerateFiltersFromTags(tags);
|
||||||
DummyBulletinBoardCallBackHandler bulletinHandler = new DummyBulletinBoardCallBackHandler();
|
DummyBulletinBoardCallBackHandler bulletinHandler = new DummyBulletinBoardCallBackHandler();
|
||||||
bulletinBoardClient.readMessages(filters, bulletinHandler);
|
bulletinBoardClient.readMessages(filters, bulletinHandler);
|
||||||
|
|
||||||
|
@ -249,7 +253,7 @@ public class SimpleRegistryTest extends TestCase {
|
||||||
* Test that remove from group creates correct bulletin board message and removes the user from a group
|
* Test that remove from group creates correct bulletin board message and removes the user from a group
|
||||||
*/
|
*/
|
||||||
public void testGetGroups() throws InvalidProtocolBufferException, InterruptedException {
|
public void testGetGroups() throws InvalidProtocolBufferException, InterruptedException {
|
||||||
DummyRegistryCallBackHandler<List<BulletinBoardAPI.BulletinBoardMessage>> handler =
|
DummyRegistryCallBackHandler<List<BulletinBoardMessage>> handler =
|
||||||
new DummyRegistryCallBackHandler<>();
|
new DummyRegistryCallBackHandler<>();
|
||||||
|
|
||||||
String voterId = new BigInteger(130, random).toString(32);
|
String voterId = new BigInteger(130, random).toString(32);
|
||||||
|
@ -258,7 +262,9 @@ public class SimpleRegistryTest extends TestCase {
|
||||||
.setVoterId(RegistryMessages.VoterID.newBuilder().setId(voterId))
|
.setVoterId(RegistryMessages.VoterID.newBuilder().setId(voterId))
|
||||||
.setGroupId(RegistryMessages.GroupID.newBuilder().setId(groupId)).build();
|
.setGroupId(RegistryMessages.GroupID.newBuilder().setId(groupId)).build();
|
||||||
|
|
||||||
SimpleRegistry registry = new SimpleRegistry(signer, bulletinBoardClient);
|
this.certStream = getClass().getResourceAsStream(CERT1_PEM_EXAMPLE);
|
||||||
|
|
||||||
|
SimpleRegistry registry = new SimpleRegistry(signer, bulletinBoardClient,certStream);
|
||||||
registry.AddToGroup(voterInfo, handler);
|
registry.AddToGroup(voterInfo, handler);
|
||||||
|
|
||||||
jobSemaphore.acquire();
|
jobSemaphore.acquire();
|
||||||
|
@ -267,7 +273,7 @@ public class SimpleRegistryTest extends TestCase {
|
||||||
DummyRegistryCallBackHandler<List<String>> groupsHandler = new DummyRegistryCallBackHandler<>();
|
DummyRegistryCallBackHandler<List<String>> groupsHandler = new DummyRegistryCallBackHandler<>();
|
||||||
registry.GetGroups(RegistryMessages.GroupID.newBuilder().setId(groupId).build(), groupsHandler);
|
registry.GetGroups(RegistryMessages.GroupID.newBuilder().setId(groupId).build(), groupsHandler);
|
||||||
|
|
||||||
jobSemaphore.acquire();
|
jobSemaphore.acquire(1);
|
||||||
List<String> userGroups = groupsHandler.data;
|
List<String> userGroups = groupsHandler.data;
|
||||||
assert userGroups.contains(RegistryTags.GROUP_ID_TAG + groupId) :
|
assert userGroups.contains(RegistryTags.GROUP_ID_TAG + groupId) :
|
||||||
"The simple voter registry object does not retrieved right user groups";
|
"The simple voter registry object does not retrieved right user groups";
|
||||||
|
@ -277,7 +283,7 @@ public class SimpleRegistryTest extends TestCase {
|
||||||
* Test that the personal data outputted about the user is right
|
* Test that the personal data outputted about the user is right
|
||||||
*/
|
*/
|
||||||
public void testGetPersonalIDDetails() throws InvalidProtocolBufferException, InterruptedException {
|
public void testGetPersonalIDDetails() throws InvalidProtocolBufferException, InterruptedException {
|
||||||
DummyRegistryCallBackHandler<List<BulletinBoardAPI.BulletinBoardMessage>> handler =
|
DummyRegistryCallBackHandler<List<BulletinBoardMessage>> handler =
|
||||||
new DummyRegistryCallBackHandler<>();
|
new DummyRegistryCallBackHandler<>();
|
||||||
|
|
||||||
String id = new BigInteger(130, random).toString(32);
|
String id = new BigInteger(130, random).toString(32);
|
||||||
|
@ -285,7 +291,7 @@ public class SimpleRegistryTest extends TestCase {
|
||||||
RegistryMessages.VoterInfo voterInfo = RegistryMessages.VoterInfo.newBuilder().
|
RegistryMessages.VoterInfo voterInfo = RegistryMessages.VoterInfo.newBuilder().
|
||||||
setId(RegistryMessages.VoterID.newBuilder().setId(id)).setInfo(data).build();
|
setId(RegistryMessages.VoterID.newBuilder().setId(id)).setInfo(data).build();
|
||||||
|
|
||||||
SimpleRegistry registry = new SimpleRegistry(signer, bulletinBoardClient);
|
SimpleRegistry registry = new SimpleRegistry(signer, bulletinBoardClient, certStream);
|
||||||
registry.AddVoter(voterInfo, handler);
|
registry.AddVoter(voterInfo, handler);
|
||||||
|
|
||||||
jobSemaphore.acquire();
|
jobSemaphore.acquire();
|
||||||
|
|
Loading…
Reference in New Issue