From b7efbea7419adf5970b77420b0c642b756145580 Mon Sep 17 00:00:00 2001 From: blavenie <benoit.lavenier@e-is.pro> Date: Wed, 25 Oct 2017 20:32:03 +0200 Subject: [PATCH] [enh] upgrade document version to 2. Now document need only be signed on hash [enh] Better JsonAttributeParser --- .../model/bma/jackson/JacksonUtils.java | 30 --- .../client/model/elasticsearch/Records.java | 1 + .../CurrencyRegistryRemoteServiceImpl.java | 8 +- .../core/util/json/JsonAttributeParser.java | 190 +++++++++++++----- .../core/util}/json/JsonArrayParserTest.java | 15 +- .../util/json/JsonAttributeParserTest.java | 74 +++++++ .../main/assembly/config/elasticsearch.yml | 8 +- .../src/test/es-home/config/elasticsearch.yml | 10 +- .../client/Duniter4jClientImpl.java | 2 +- .../service/AbstractService.java | 65 ++++-- .../service/BlockchainService.java | 39 ++-- .../synchro/AbstractSynchroAction.java | 3 +- .../user/service/UserEventService.java | 6 +- 13 files changed, 315 insertions(+), 136 deletions(-) rename {duniter4j-core-client/src/test/java/org/duniter/core/client/model/bma => duniter4j-core-shared/src/test/java/org/duniter/core/util}/json/JsonArrayParserTest.java (69%) create mode 100644 duniter4j-core-shared/src/test/java/org/duniter/core/util/json/JsonAttributeParserTest.java diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/JacksonUtils.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/JacksonUtils.java index 152cbc1b..31821fda 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/JacksonUtils.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/JacksonUtils.java @@ -26,17 +26,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import org.duniter.core.client.model.bma.BlockchainBlock; import org.duniter.core.client.model.bma.NetworkPeering; -import org.duniter.core.util.json.JsonArrayParser; -import org.duniter.core.util.json.JsonAttributeParser; - -import java.util.List; /** * Created by blavenie on 07/12/16. */ public abstract class JacksonUtils extends SimpleModule { - public static final String REGEX_ATTRIBUTE_REPLACE = "[,]?(?:\"%s\"|%s)[\\s\\n\\r]*:[\\s\\n\\r]*(?:\"[^\"]+\"|null)"; private static final ThreadLocal<ObjectMapper> mapper = new ThreadLocal<ObjectMapper>() { @@ -73,29 +68,4 @@ public abstract class JacksonUtils extends SimpleModule { return objectMapper; } - public static List<String> getValuesFromJSONAsString(String jsonString, String attributeName) { - return new JsonAttributeParser(attributeName).getValues(jsonString); - } - - public static String getValueFromJSONAsString(String jsonString, String attributeName) { - return new JsonAttributeParser(attributeName).getValueAsString(jsonString); - } - - public static Number getValueFromJSONAsNumber(String jsonString, String attributeName) { - return new JsonAttributeParser(attributeName).getValueAsNumber(jsonString); - } - - public static int getValueFromJSONAsInt(String jsonString, String attributeName) { - return new JsonAttributeParser(attributeName).getValueAsInt(jsonString); - } - - public static List<String> getArrayValuesFromJSONAsInt(String jsonString) { - return new JsonArrayParser().getValuesAsList(jsonString); - } - - public static String removeAttribute(String jsonString, String attributeName) { - String regex = String.format(REGEX_ATTRIBUTE_REPLACE, attributeName, attributeName); - return jsonString.replaceAll(regex, ""); - } - } diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/Records.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/Records.java index 95f72325..0fbc3c93 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/Records.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/elasticsearch/Records.java @@ -31,6 +31,7 @@ public final class Records { public static final String PROPERTY_ISSUER="issuer"; public static final String PROPERTY_HASH="hash"; public static final String PROPERTY_SIGNATURE="signature"; + public static final String PROPERTY_VERSION="version"; public static final String PROPERTY_TIME="time"; public static final String PROPERTY_READ_SIGNATURE="read_signature"; diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/elasticsearch/CurrencyRegistryRemoteServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/elasticsearch/CurrencyRegistryRemoteServiceImpl.java index 47c81eef..15a8c832 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/elasticsearch/CurrencyRegistryRemoteServiceImpl.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/elasticsearch/CurrencyRegistryRemoteServiceImpl.java @@ -26,10 +26,10 @@ import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpGet; import org.duniter.core.beans.InitializingBean; import org.duniter.core.client.config.Configuration; -import org.duniter.core.client.model.bma.jackson.JacksonUtils; import org.duniter.core.client.model.local.Peer; import org.duniter.core.client.service.bma.BaseRemoteServiceImpl; import org.duniter.core.exception.TechnicalException; +import org.duniter.core.util.json.JsonAttributeParser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -83,8 +83,8 @@ public class CurrencyRegistryRemoteServiceImpl extends BaseRemoteServiceImpl imp String jsonResponse; try { jsonResponse = executeRequest(peer, URL_STATUS, String.class); - int statusCode = JacksonUtils.getValueFromJSONAsInt(jsonResponse, "status"); - return statusCode == HttpStatus.SC_OK; + Integer statusCode = new JsonAttributeParser<>("status", Integer.class).getValue(jsonResponse); + return statusCode != null && statusCode == HttpStatus.SC_OK; } catch(TechnicalException e) { if (log.isDebugEnabled()) { @@ -104,7 +104,7 @@ public class CurrencyRegistryRemoteServiceImpl extends BaseRemoteServiceImpl imp String path = getPath(peer, URL_ALL_CURRENCY_NAMES); String jsonResponse = executeRequest(new HttpGet(path), String.class); - List<String> currencyNames = JacksonUtils.getValuesFromJSONAsString(jsonResponse, "currencyName"); + List<String> currencyNames = new JsonAttributeParser<>("currencyName", String.class).getValues(jsonResponse); // Sort into alphabetical order Collections.sort(currencyNames); diff --git a/duniter4j-core-shared/src/main/java/org/duniter/core/util/json/JsonAttributeParser.java b/duniter4j-core-shared/src/main/java/org/duniter/core/util/json/JsonAttributeParser.java index 62b04ec8..c227e22f 100644 --- a/duniter4j-core-shared/src/main/java/org/duniter/core/util/json/JsonAttributeParser.java +++ b/duniter4j-core-shared/src/main/java/org/duniter/core/util/json/JsonAttributeParser.java @@ -25,6 +25,8 @@ package org.duniter.core.util.json; import org.duniter.core.exception.TechnicalException; import org.duniter.core.util.Preconditions; +import javax.print.DocFlavor; +import java.math.BigDecimal; import java.text.DecimalFormat; import java.text.ParseException; import java.util.ArrayList; @@ -32,69 +34,163 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -public class JsonAttributeParser { +public class JsonAttributeParser<T extends Object> { - public static final String REGEX_ATTRIBUTE_STRING_VALUE = "\\\"%s\\\"\\s*:\\s*\"([^\"]+)\\\""; - public static final String REGEX_ATTRIBUTE_NUMERIC_VALUE = "\\\"%s\\\"\\s*:\\s*([\\d]+(?:[.][\\d]+)?)"; + public enum Type { + INTEGER, + LONG, + DOUBLE, + BIGDECIMAL, + BOOLEAN, + STRING + } - private Pattern pattern; - private Pattern numericPattern; - private DecimalFormat decimalFormat; - private String attributeName; + public static final String REGEX_ATTRIBUTE_STRING_VALUE = "\\\"%s\\\"\\s*:\\s*\"([^\"]+)\\\""; + public static final String REGEX_ATTRIBUTE_NUMERIC_VALUE = "\\\"%s\\\"\\s*:\\s*([\\d]+(?:[.][\\d]+)?)"; + public static final String REGEX_ATTRIBUTE_BOOLEAN_VALUE = "\\\"%s\\\"\\s*:\\s*(true|false)"; - public JsonAttributeParser(String attributeName) { - Preconditions.checkNotNull(attributeName); + private Type type; + private Pattern pattern; + private DecimalFormat decimalFormat; + private String attributeName; - this.attributeName = attributeName; - this.numericPattern = Pattern.compile(String.format(REGEX_ATTRIBUTE_NUMERIC_VALUE, attributeName)); + public JsonAttributeParser(String attributeName, Class<? extends T> clazz) { + Preconditions.checkNotNull(attributeName); + + this.attributeName = attributeName; + + // String + if (String.class.isAssignableFrom(clazz)) { + type = Type.STRING; this.pattern = Pattern.compile(String.format(REGEX_ATTRIBUTE_STRING_VALUE, attributeName)); + } + // Integer + else if (Integer.class.isAssignableFrom(clazz)) { + type = Type.INTEGER; + this.pattern = Pattern.compile(String.format(REGEX_ATTRIBUTE_NUMERIC_VALUE, attributeName)); + this.decimalFormat = new DecimalFormat(); + this.decimalFormat.setParseIntegerOnly(true); + } + // Long + else if (Long.class.isAssignableFrom(clazz)) { + type = Type.LONG; + this.pattern = Pattern.compile(String.format(REGEX_ATTRIBUTE_NUMERIC_VALUE, attributeName)); + this.decimalFormat = new DecimalFormat(); + } + // Double + else if (Double.class.isAssignableFrom(clazz)) { + type = Type.DOUBLE; + this.pattern = Pattern.compile(String.format(REGEX_ATTRIBUTE_NUMERIC_VALUE, attributeName)); + this.decimalFormat = new DecimalFormat(); + this.decimalFormat.getDecimalFormatSymbols().setDecimalSeparator('.'); + } + // BigDecimal + else if (BigDecimal.class.isAssignableFrom(clazz)) { + type = Type.BIGDECIMAL; + this.pattern = Pattern.compile(String.format(REGEX_ATTRIBUTE_NUMERIC_VALUE, attributeName)); this.decimalFormat = new DecimalFormat(); + this.decimalFormat.setParseBigDecimal(true); // allow big decimal this.decimalFormat.getDecimalFormatSymbols().setDecimalSeparator('.'); } + // Boolean + else if (Boolean.class.isAssignableFrom(clazz)) { + type = Type.BOOLEAN; + this.pattern = Pattern.compile(String.format(REGEX_ATTRIBUTE_BOOLEAN_VALUE, attributeName)); + } + else { + throw new IllegalArgumentException("Invalid attribute class " + clazz.getCanonicalName()); + } + } + + public T getValue(String jsonString) { + Preconditions.checkNotNull(jsonString); - public Number getValueAsNumber(String jsonString) { - Preconditions.checkNotNull(jsonString); - - Matcher matcher = numericPattern.matcher(jsonString); - - if (!matcher.find()) { - return null; - } - String group = matcher.group(1); - try { - Number result = decimalFormat.parse(group); - return result; - } catch (ParseException e) { - throw new TechnicalException(String.format("Error while parsing json numeric value, for attribute [%s]: %s", attributeName,e.getMessage()), e); - } + Matcher matcher = pattern.matcher(jsonString); + + if (!matcher.find()) { + return null; } - public int getValueAsInt(String jsonString) { - Number numberValue = getValueAsNumber(jsonString); - if (numberValue == null) { - return 0; - } - return numberValue.intValue(); + return parseValue(matcher.group(1)); + } + + public List<T> getValues(String jsonString) { + Preconditions.checkArgument(type == Type.STRING); + + Matcher matcher = pattern.matcher(jsonString); + List<T> result = new ArrayList<T>(); + while (matcher.find()) { + String strValue = matcher.group(1); + result.add(parseValue(strValue)); } - public String getValueAsString(String jsonString) { - Matcher matcher = pattern.matcher(jsonString); - if (!matcher.find()) { - return null; - } + return result; + } - return matcher.group(1); + public String removeFromJson(final String jsonString) { + Matcher matcher = pattern.matcher(jsonString); + if (!matcher.find()) { + return jsonString; } - public List<String> getValues(String jsonString) { - Matcher matcher = pattern.matcher(jsonString); - List<String> result = new ArrayList<>(); - while (matcher.find()) { - String group = matcher.group(1); - result.add(group); - } + int start = matcher.start(); + int end = matcher.end(); - return result; + char before = jsonString.charAt(start-1); + while (before == ',' || before == ' ' || before == '\t' || before == '\n') { + before = jsonString.charAt(--start-1); + } + char after = jsonString.charAt(end); + while (after == ',' || after == ' ' || after == '\t' || after == '\n') { + after = jsonString.charAt(++end); } - } \ No newline at end of file + StringBuilder sb = new StringBuilder(); + sb.append(jsonString.substring(0, start)); + sb.append(jsonString.substring(end)); + return sb.toString(); + } + + /* -- private methods -- */ + + private T parseValue(String attributeValue) { + + switch(type) { + case STRING: + return (T)attributeValue; + case INTEGER: + try { + Number result = decimalFormat.parse(attributeValue); + return (T)new Integer(result.intValue()); + } catch (ParseException e) { + throw new TechnicalException(String.format("Error while parsing json numeric value, for attribute [%s]: %s", attributeName,e.getMessage()), e); + } + case LONG: + try { + Number result = decimalFormat.parse(attributeValue); + return (T)new Long(result.longValue()); + } catch (ParseException e) { + throw new TechnicalException(String.format("Error while parsing json numeric value, for attribute [%s]: %s", attributeName,e.getMessage()), e); + } + case DOUBLE: + try { + Number result = decimalFormat.parse(attributeValue); + return (T)new Double(result.doubleValue()); + } catch (ParseException e) { + throw new TechnicalException(String.format("Error while parsing json numeric value, for attribute [%s]: %s", attributeName,e.getMessage()), e); + } + case BIGDECIMAL: + try { + Number result = decimalFormat.parse(attributeValue); + return (T)result; + } catch (ParseException e) { + throw new TechnicalException(String.format("Error while parsing json numeric value, for attribute [%s]: %s", attributeName,e.getMessage()), e); + } + case BOOLEAN: + return (T)new Boolean(attributeValue); + } + + return null; + } + +} \ No newline at end of file diff --git a/duniter4j-core-client/src/test/java/org/duniter/core/client/model/bma/json/JsonArrayParserTest.java b/duniter4j-core-shared/src/test/java/org/duniter/core/util/json/JsonArrayParserTest.java similarity index 69% rename from duniter4j-core-client/src/test/java/org/duniter/core/client/model/bma/json/JsonArrayParserTest.java rename to duniter4j-core-shared/src/test/java/org/duniter/core/util/json/JsonArrayParserTest.java index b7fac04d..3d1dbdeb 100644 --- a/duniter4j-core-client/src/test/java/org/duniter/core/client/model/bma/json/JsonArrayParserTest.java +++ b/duniter4j-core-shared/src/test/java/org/duniter/core/util/json/JsonArrayParserTest.java @@ -1,4 +1,4 @@ -package org.duniter.core.client.model.bma.json; +package org.duniter.core.util.json; /* * #%L @@ -22,7 +22,6 @@ package org.duniter.core.client.model.bma.json; * #L% */ -import org.duniter.core.util.json.JsonArrayParser; import org.junit.Assert; import org.junit.Test; @@ -31,19 +30,21 @@ import org.junit.Test; */ public class JsonArrayParserTest { + private static String OBJ_JSON = "{'id':'joe','ts':'2014-12-02T13:58:23.801+0100','foo':{'bar':{'v1':50019820,'v2':0, 'v3':0.001, 'v4':-100, 'v5':0.000001, 'v6':0.0, 'b':true}}}".replace("'", "\""); + @Test - public void getValues() { - String obj = "{'id':'joe','ts':'2014-12-02T13:58:23.801+0100','foo':{'bar':{'v1':50019820,'v2':0, 'v3':0.001, 'v4':-100, 'v5':0.000001, 'v6':0.0, 'b':true}}}".replace("'", "\""); - String string = String.format("[%s,%s,%s,%s]", obj , obj , obj , obj ); + public void getValuesAsArray() { + String jsonString = String.format("[%s,%s,%s,%s]", OBJ_JSON , OBJ_JSON , OBJ_JSON , OBJ_JSON ); JsonArrayParser parser = new JsonArrayParser(); - String[] result = parser.getValuesAsArray(string); + String[] result = parser.getValuesAsArray(jsonString); Assert.assertNotNull(result); Assert.assertEquals(4, result.length); - Assert.assertEquals(obj, result[0]); + Assert.assertEquals(OBJ_JSON, result[0]); result = parser.getValuesAsArray("[]"); Assert.assertNull(result); } + } diff --git a/duniter4j-core-shared/src/test/java/org/duniter/core/util/json/JsonAttributeParserTest.java b/duniter4j-core-shared/src/test/java/org/duniter/core/util/json/JsonAttributeParserTest.java new file mode 100644 index 00000000..d74d88a4 --- /dev/null +++ b/duniter4j-core-shared/src/test/java/org/duniter/core/util/json/JsonAttributeParserTest.java @@ -0,0 +1,74 @@ +package org.duniter.core.util.json; + +/* + * #%L + * UCoin Java :: Core Client API + * %% + * Copyright (C) 2014 - 2016 EIS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * <http://www.gnu.org/licenses/gpl-3.0.html>. + * #L% + */ + +import org.junit.Assert; +import org.junit.Test; + +/** + * Created by blavenie on 05/01/16. + */ +public class JsonAttributeParserTest { + + private static final String PROPERTY_ID = "id"; + private static final String PROPERTY_TS = "ts"; + private static final String PROPERTY_B = "b"; + + private static final String TS_VALUE = "2014-12-02T13:58:23.801+0100"; + private static final String OBJ_JSON = ("{'id':'joe','ts':'" + TS_VALUE + "','foo':{'bar':{'v1':50019820,'v2':0, 'v3':0.001, 'v4':-100, 'v5':0.000001, 'v6':0.0, 'b':true}}}") + .replace("'", "\""); + + @Test + public void getValueAsString() { + String jsonString = String.format("%s", OBJ_JSON); + + JsonAttributeParser<String> isAttribute = new JsonAttributeParser<>(PROPERTY_ID, String.class); + String idValue = isAttribute.getValue(jsonString); + Assert.assertEquals("joe", idValue); + + } + + @Test + public void removeStringAttributeFromJson() { + String jsonString = String.format("%s", OBJ_JSON); + String expectedJson = jsonString.replace("\"id\":\"joe\",", ""); + expectedJson = expectedJson.replace(", \"b\":true", ""); + + // Remove 'id' + JsonAttributeParser<String> idAttribute = new JsonAttributeParser<>(PROPERTY_ID, String.class); + String newJson = idAttribute.removeFromJson(jsonString); + + // Remove 'b' + JsonAttributeParser<Boolean> bAttribute = new JsonAttributeParser<>(PROPERTY_B, Boolean.class); + newJson = bAttribute.removeFromJson(newJson); + + Assert.assertEquals(expectedJson, newJson); + + // Remove 'ts' + JsonAttributeParser<String> tsAttribute = new JsonAttributeParser<>(PROPERTY_TS, String.class); + newJson = tsAttribute.removeFromJson(newJson); + + expectedJson = expectedJson.replace("\"ts\":\""+TS_VALUE+"\",", ""); + Assert.assertEquals(expectedJson, newJson); + } +} diff --git a/duniter4j-es-assembly/src/main/assembly/config/elasticsearch.yml b/duniter4j-es-assembly/src/main/assembly/config/elasticsearch.yml index 114d3f70..a7f7c95c 100644 --- a/duniter4j-es-assembly/src/main/assembly/config/elasticsearch.yml +++ b/duniter4j-es-assembly/src/main/assembly/config/elasticsearch.yml @@ -119,7 +119,7 @@ security.manager.enabled: false # duniter.string.analyzer: french # -# Enabling blockchain synchronization +# Enabling blockchain synchronization (default: false) # duniter.blockchain.enable: true # @@ -181,15 +181,15 @@ duniter.p2p.includes.endpoints: [ # # ---------------------------------- Duniter4j document moderation --------------- # -# Filter too old document, if time older that 'maxPastDelta' (in seconds). Default: 7200 (=2h) +# Filter too old document, if time older that 'maxPastDelta' (in seconds). (default: 7200 =2h) # # duniter.document.time.maxPastDelta: 7200 # -# Filter document in the futur, if time greater that 'maxFutureDelta' (in seconds). Default: 600 (10m) +# Filter document in the futur, if time greater that 'maxFutureDelta' (in seconds). (default: 600 =10min) # # duniter.document.time.maxFutureDelta: 600 # -# Allow admin (define in duniter.keyring) to delete documents ? Default: false +# Allow admin (define in duniter.keyring) to delete documents ? (default: true) # # duniter.document.allowAdminDeletion: true # diff --git a/duniter4j-es-assembly/src/test/es-home/config/elasticsearch.yml b/duniter4j-es-assembly/src/test/es-home/config/elasticsearch.yml index 589d5d48..553a93e4 100644 --- a/duniter4j-es-assembly/src/test/es-home/config/elasticsearch.yml +++ b/duniter4j-es-assembly/src/test/es-home/config/elasticsearch.yml @@ -178,10 +178,12 @@ duniter.p2p.discovery.enable: false # # Pass a list of hosts to always synchronize (default: <empty>) # -#duniter.p2p.includes.endpoints: [ -# "ES_USER_API g1.data.duniter.fr 443", -# "ES_SUBSCRIPTION_API g1.data.duniter.fr 443" -#] +duniter.p2p.includes.endpoints: [ + "ES_USER_API g1.data.duniter.fr 443", + "ES_SUBSCRIPTION_API g1.data.duniter.fr 443", + "ES_USER_API g1.data.le-sou.org 443", + "ES_SUBSCRIPTION_API g1.data.le-sou.org 443" +] # # Pass a list of pubkeys to always synchronize (default: <empty>) # diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/client/Duniter4jClientImpl.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/client/Duniter4jClientImpl.java index 6af177c6..adb095e8 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/client/Duniter4jClientImpl.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/client/Duniter4jClientImpl.java @@ -198,7 +198,7 @@ public class Duniter4jClientImpl implements Duniter4jClient { public void checkSameDocumentIssuer(String index, String type, String id, String expectedIssuer) { String issuer = getMandatoryFieldsById(index, type, id, Record.PROPERTY_ISSUER).get(Record.PROPERTY_ISSUER).toString(); if (!ObjectUtils.equals(expectedIssuer, issuer)) { - throw new TechnicalException("Not same issuer"); + throw new AccessDeniedException("Not same issuer"); } } diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractService.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractService.java index a0616ed9..ea9cb6c6 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractService.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractService.java @@ -33,6 +33,7 @@ import org.duniter.core.client.model.elasticsearch.Record; import org.duniter.core.client.model.elasticsearch.Records; import org.duniter.core.exception.TechnicalException; import org.duniter.core.service.CryptoService; +import org.duniter.core.util.json.JsonAttributeParser; import org.duniter.elasticsearch.PluginSettings; import org.duniter.elasticsearch.client.Duniter4jClient; import org.duniter.elasticsearch.exception.InvalidFormatException; @@ -44,6 +45,7 @@ import org.elasticsearch.common.logging.Loggers; import org.nuiton.i18n.I18n; import java.io.IOException; +import java.util.Objects; import java.util.Set; /** @@ -51,16 +53,20 @@ import java.util.Set; */ public abstract class AbstractService implements Bean { - protected final ESLogger logger; + protected static JsonAttributeParser<String> PARSER_HASH = new JsonAttributeParser<>(Record.PROPERTY_HASH, String.class); + protected static JsonAttributeParser<String> PARSER_SIGNATURE = new JsonAttributeParser<>(Record.PROPERTY_SIGNATURE, String.class); + protected static JsonAttributeParser<String> PARSER_READ_SIGNATURE = new JsonAttributeParser<>(Records.PROPERTY_READ_SIGNATURE, String.class); + protected final ESLogger logger; protected Duniter4jClient client; protected PluginSettings pluginSettings; protected CryptoService cryptoService; - protected final int retryCount; - protected final int retryWaitDuration; - protected final int documentTimeMaxPastDelta; - protected final int documentTimeMaxFutureDelta; - protected boolean ready = false; + + private boolean ready = false; + private final int retryCount; + private final int retryWaitDuration; + private final int documentTimeMaxPastDelta; + private final int documentTimeMaxFutureDelta; public AbstractService(String loggerName, Duniter4jClient client, PluginSettings pluginSettings) { this(loggerName, client, pluginSettings, null); @@ -212,6 +218,14 @@ public abstract class AbstractService implements Bean { return getMandatoryField(actualObj, Records.PROPERTY_ISSUER).asText(); } + protected int getVersion(JsonNode actualObj) { + JsonNode value = actualObj.get(Records.PROPERTY_VERSION); + if (value == null || value.isMissingNode()) { + return 1; // first version + } + return value.asInt(); + } + protected JsonNode getMandatoryField(JsonNode actualObj, String fieldName) { JsonNode value = actualObj.get(fieldName); if (value.isMissingNode()) { @@ -236,28 +250,49 @@ public abstract class AbstractService implements Bean { } String issuer = getMandatoryField(recordObj, issuerFieldName).asText(); String signature = getMandatoryField(recordObj, Records.PROPERTY_SIGNATURE).asText(); + String hash = getMandatoryField(recordObj, Records.PROPERTY_HASH).asText(); + int version = getVersion(recordObj); + + boolean validSignature = false; // Remove hash and signature - recordJson = JacksonUtils.removeAttribute(recordJson, Records.PROPERTY_SIGNATURE); - recordJson = JacksonUtils.removeAttribute(recordJson, Records.PROPERTY_HASH); + recordJson = PARSER_SIGNATURE.removeFromJson(recordJson); + recordJson = PARSER_HASH.removeFromJson(recordJson); // Remove 'read_signature' attribute if exists (added AFTER signature) + String readSignature = null; if (fieldNames.contains(Records.PROPERTY_READ_SIGNATURE)) { - recordJson = JacksonUtils.removeAttribute(recordJson, Records.PROPERTY_READ_SIGNATURE); + readSignature = getMandatoryField(recordObj, Records.PROPERTY_READ_SIGNATURE).asText(); + recordJson = PARSER_READ_SIGNATURE.removeFromJson(recordJson); } - if (!cryptoService.verify(recordJson, signature, issuer)) { + // Doc version == 1 + if (version == 1) { + validSignature = cryptoService.verify(recordJson, signature, issuer); + } - if (recordJson.contains("\"socials\":[]")) { - recordJson = recordJson.replaceAll(",\"socials\":\\[\\]", ""); - if (cryptoService.verify(recordJson, signature, issuer)) { - return; // ok - } + // Doc version > 1 + else { + // Remove hash and signature + boolean validHash = Objects.equals(cryptoService.hash(recordJson), hash); + if (!validHash) { + throw new InvalidSignatureException("Invalid hash of JSON document"); } + // Validate signature on hash + validSignature = cryptoService.verify(hash, signature, issuer); + } + + if (!validSignature) { + throw new InvalidSignatureException("Invalid signature of JSON string"); } + // Validate read signature on hash + if (readSignature != null) { + // TODO: validate read signature / recipient ? + } + // TODO: check issuer is in the WOT ? } } diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/BlockchainService.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/BlockchainService.java index d0e24d97..c9dbda70 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/BlockchainService.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/BlockchainService.java @@ -73,10 +73,10 @@ public class BlockchainService extends AbstractService { private List<WebsocketClientEndpoint.ConnectionListener> connectionListeners = new ArrayList<>(); private final WebsocketClientEndpoint.ConnectionListener dispatchConnectionListener; - private final JsonAttributeParser blockNumberParser = new JsonAttributeParser("number"); - private final JsonAttributeParser blockCurrencyParser = new JsonAttributeParser("currency"); - private final JsonAttributeParser blockHashParser = new JsonAttributeParser("hash"); - private final JsonAttributeParser blockPreviousHashParser = new JsonAttributeParser("previousHash"); + private final JsonAttributeParser<Integer> blockNumberParser = new JsonAttributeParser<>("number", Integer.class); + private final JsonAttributeParser<String> blockCurrencyParser = new JsonAttributeParser<>("currency", String.class); + private final JsonAttributeParser<String> blockHashParser = new JsonAttributeParser<>("hash", String.class); + private final JsonAttributeParser<String> blockPreviousHashParser = new JsonAttributeParser<>("previousHash", String.class); private BlockDao blockDao; @@ -307,10 +307,11 @@ public class BlockchainService extends AbstractService { Preconditions.checkNotNull(json); Preconditions.checkArgument(json.length() > 0); - String currencyName = blockCurrencyParser.getValueAsString(json); - int number = blockNumberParser.getValueAsInt(json); - String hash = blockHashParser.getValueAsString(json); + String currencyName = blockCurrencyParser.getValue(json); + Integer number = blockNumberParser.getValue(json); + String hash = blockHashParser.getValue(json); + Preconditions.checkNotNull(number); logger.info(I18n.t("duniter4j.blockIndexerService.indexBlock", currencyName, peer, number, hash)); if (logger.isTraceEnabled()) { logger.trace(json); @@ -318,7 +319,7 @@ public class BlockchainService extends AbstractService { // Detecting fork and rollback is necessary if (detectFork) { - String previousHash = blockPreviousHashParser.getValueAsString(json); + String previousHash = blockPreviousHashParser.getValue(json); boolean resolved = detectAndResolveFork(peer, currencyName, previousHash, number - 1); if (!resolved) { // Bad blockchain ! Skipping block indexation @@ -404,7 +405,7 @@ public class BlockchainService extends AbstractService { /* -- Internal methods -- */ - protected Collection<String> indexBlocksNoBulk(Peer peer, String currencyName, int firstNumber, int lastNumber, ProgressionModel progressionModel) { + private Collection<String> indexBlocksNoBulk(Peer peer, String currencyName, int firstNumber, int lastNumber, ProgressionModel progressionModel) { Set<String> missingBlockNumbers = new LinkedHashSet<>(); for (int curNumber = firstNumber; curNumber <= lastNumber; curNumber++) { @@ -442,7 +443,7 @@ public class BlockchainService extends AbstractService { return missingBlockNumbers; } - protected Collection<String> indexBlocksUsingBulk(Peer peer, String currencyName, int firstNumber, int lastNumber, ProgressionModel progressionModel) { + private Collection<String> indexBlocksUsingBulk(Peer peer, String currencyName, int firstNumber, int lastNumber, ProgressionModel progressionModel) { Set<String> missingBlockNumbers = new LinkedHashSet<>(); boolean debug = logger.isDebugEnabled(); @@ -486,16 +487,16 @@ public class BlockchainService extends AbstractService { List<Integer> processedBlockNumbers = Lists.newArrayList(); BulkRequestBuilder bulkRequest = client.prepareBulk(); for (String blockAsJson : blocksAsJson) { - int itemNumber = blockNumberParser.getValueAsInt(blockAsJson); + Integer itemNumber = blockNumberParser.getValue(blockAsJson); // update curNumber with max number; if (itemNumber > batchFirstNumber) { batchFirstNumber = itemNumber; } - if (!processedBlockNumbers.contains(itemNumber)) { + if (itemNumber != null && !processedBlockNumbers.contains(itemNumber)) { // Add to bulk - bulkRequest.add(client.prepareIndex(currencyName, BLOCK_TYPE, String.valueOf(itemNumber)) + bulkRequest.add(client.prepareIndex(currencyName, BLOCK_TYPE, itemNumber.toString()) .setRefresh(false) // recommended for heavy indexing .setSource(blockAsJson) ); @@ -552,7 +553,7 @@ public class BlockchainService extends AbstractService { * @param sortedMissingBlocks * @param tryCounter */ - protected Collection<String> indexMissingBlocksFromOtherPeers(Peer peer, BlockchainBlock currentBlock, Collection<String> sortedMissingBlocks, int tryCounter) { + private Collection<String> indexMissingBlocksFromOtherPeers(Peer peer, BlockchainBlock currentBlock, Collection<String> sortedMissingBlocks, int tryCounter) { Preconditions.checkNotNull(peer); Preconditions.checkNotNull(currentBlock); Preconditions.checkNotNull(currentBlock.getHash()); @@ -668,7 +669,7 @@ public class BlockchainService extends AbstractService { return indexMissingBlocksFromOtherPeers(peer, newCurrentBlock, newMissingBlocks, tryCounter); } - protected void reportIndexBlocksProgress(ProgressionModel progressionModel, String currencyName, Peer peer, int firstNumber, int lastNumber, int curNumber) { + private void reportIndexBlocksProgress(ProgressionModel progressionModel, String currencyName, Peer peer, int firstNumber, int lastNumber, int curNumber) { int pct = (curNumber - firstNumber) * 100 / (lastNumber - firstNumber); progressionModel.setCurrent(pct); @@ -679,7 +680,7 @@ public class BlockchainService extends AbstractService { } - protected boolean isBlockIndexed(String currencyName, int number, String hash) { + private boolean isBlockIndexed(String currencyName, int number, String hash) { Preconditions.checkNotNull(currencyName); Preconditions.checkNotNull(hash); // Check if previous block exists @@ -691,7 +692,7 @@ public class BlockchainService extends AbstractService { return ObjectUtils.equals(block.getHash(), hash); } - protected boolean detectAndResolveFork(Peer peer, final String currencyName, final String hash, final int number){ + private boolean detectAndResolveFork(Peer peer, final String currencyName, final String hash, final int number){ int forkResyncWindow = pluginSettings.getNodeForkResyncWindow(); String forkOriginHash = hash; int forkOriginNumber = number; @@ -711,7 +712,7 @@ public class BlockchainService extends AbstractService { final int currentNumberFinal = forkOriginNumber; String testBlock = executeWithRetry(() -> blockchainRemoteService.getBlockAsJson(peer, currentNumberFinal)); - forkOriginHash = blockHashParser.getValueAsString(testBlock); + forkOriginHash = blockHashParser.getValue(testBlock); // Check is exists on ES index sameBlockIndexed = isBlockIndexed(currencyName, forkOriginNumber, forkOriginHash); @@ -738,7 +739,7 @@ public class BlockchainService extends AbstractService { } - protected String getBlockId(int number) { + private String getBlockId(int number) { return number == -1 ? CURRENT_BLOCK_ID : String.valueOf(number); } } diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/synchro/AbstractSynchroAction.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/synchro/AbstractSynchroAction.java index 252f37c6..537f2f80 100644 --- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/synchro/AbstractSynchroAction.java +++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/synchro/AbstractSynchroAction.java @@ -514,12 +514,11 @@ public abstract class AbstractSynchroAction extends AbstractService implements S // Execute update UpdateRequestBuilder request = client.prepareUpdate(toIndex, toType, id); + request.setDoc(objectMapper.writeValueAsBytes(source)); if (bulkRequest != null) { - request.setDoc(objectMapper.writeValueAsBytes(source)); bulkRequest.add(request); } else { - request.setSource(objectMapper.writeValueAsBytes(source)); request.setRefresh(true); client.safeExecuteRequest(request, false); } diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserEventService.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserEventService.java index 1b138cbf..3aaed432 100644 --- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserEventService.java +++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserEventService.java @@ -93,7 +93,7 @@ public class UserEventService extends AbstractService implements ChangeService.C } private final ThreadPool threadPool; - public final boolean trace; + private final boolean trace; @Inject public UserEventService(final Duniter4jClient client, @@ -450,8 +450,8 @@ public class UserEventService extends AbstractService implements ChangeService.C try { String json = getObjectMapper().writeValueAsString(userEvent); if (cleanHashAndSignature) { - json = JacksonUtils.removeAttribute(json, Record.PROPERTY_SIGNATURE); - json = JacksonUtils.removeAttribute(json, Record.PROPERTY_HASH); + json = PARSER_SIGNATURE.removeFromJson(json); + json = PARSER_HASH.removeFromJson(json); } return json; } catch(JsonProcessingException e) { -- GitLab