diff --git a/duniter4j-client/src/main/java/org/duniter/client/actions/NetworkAction.java b/duniter4j-client/src/main/java/org/duniter/client/actions/NetworkAction.java index 2c033bea52db11eed0baa83bcc6fc490b8729e3f..e329116dd279e3f1cfd3a8d91c33b8f4cecdf487 100644 --- a/duniter4j-client/src/main/java/org/duniter/client/actions/NetworkAction.java +++ b/duniter4j-client/src/main/java/org/duniter/client/actions/NetworkAction.java @@ -175,7 +175,7 @@ public class NetworkAction extends AbstractAction { peer.getStats().getStatus().name(), isUp ? formatApi(peer) : "", isUp ? peer.getStats().getVersion() : "", - (isUp && peer.getStats().getHardshipLevel() != null) ? peer.getStats().getHardshipLevel() : I18n.t("duniter4j.client.network.mirror"), + (isUp && peer.getStats().getHardshipLevel() != null) ? peer.getStats().getHardshipLevel() : (peer.getStats().getUid() == null ? I18n.t("duniter4j.client.network.mirror") : ""), isUp ? formatBuid(peer.getStats()) : "" }; }) diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkWs2pHeads.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkWs2pHeads.java new file mode 100644 index 0000000000000000000000000000000000000000..948d313f7fc6d2dc316ae7e2e34a386c04638202 --- /dev/null +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/NetworkWs2pHeads.java @@ -0,0 +1,97 @@ +package org.duniter.core.client.model.bma; + +/* + * #%L + * Duniter4j :: Core Client API + * %% + * Copyright (C) 2014 - 2017 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 java.io.Serializable; + +/** + * Created by blavenie on 22/01/19. + */ +public class NetworkWs2pHeads { + + public NetworkWs2pHeads.Head[] heads; + + public String toString() { + StringBuilder sb = new StringBuilder(); + for(NetworkWs2pHeads.Head head : heads) { + sb.append(head.toString()).append("\n"); + } + return sb.toString(); + } + + public static class Head implements Serializable { + public Ws2pHead message; + public String sig; + public String messageV2; + public String sigV2; + public Integer step; + + public Ws2pHead getMessage() { + return message; + } + + public void setMessage(Ws2pHead message) { + this.message = message; + } + + public String getSig() { + return sig; + } + + public void setSig(String sig) { + this.sig = sig; + } + + public String getMessageV2() { + return messageV2; + } + + public void setMessageV2(String messageV2) { + this.messageV2 = messageV2; + } + + public String getSigV2() { + return sigV2; + } + + public void setSigV2(String sigV2) { + this.sigV2 = sigV2; + } + + public Integer getStep() { + return step; + } + + public void setStep(Integer step) { + this.step = step; + } + + @Override + public String toString() { + String s = "message=" + message + "\n" + + "sig=" + sig+ "\n" + + "step=" + step; + return s; + } + } +} \ No newline at end of file diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Ws2pHead.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Ws2pHead.java new file mode 100644 index 0000000000000000000000000000000000000000..0813f9327129efe1a71c1e6c221dc6aff7bc7225 --- /dev/null +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Ws2pHead.java @@ -0,0 +1,199 @@ +package org.duniter.core.client.model.bma; + +/* + * #%L + * Duniter4j :: Core Client API + * %% + * Copyright (C) 2014 - 2017 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 com.fasterxml.jackson.annotation.JsonIgnore; +import com.google.common.base.Joiner; + +import java.io.Serializable; + +/** + * Created by blavenie on 22/01/19. + */ +public class Ws2pHead implements Serializable { + + + public class AccessConfig { + public boolean useTor; + private String mode; + + public boolean isUseTor() { + return useTor; + } + + public void setUseTor(boolean useTor) { + this.useTor = useTor; + } + + public String getMode() { + return mode; + } + + public void setMode(String mode) { + this.mode = mode; + } + } + + public Integer version; + public String pubkey; + public String block; + public String ws2pid; + public String software; + public String softwareVersion; + public String powPrefix; + + public String signature; + + public AccessConfig privateConfig = new AccessConfig(); + public AccessConfig publicConfig = new AccessConfig(); + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public String getPubkey() { + return pubkey; + } + + public void setPubkey(String pubkey) { + this.pubkey = pubkey; + } + + public String getBlock() { + return block; + } + + public void setBlock(String block) { + this.block = block; + } + + public String getWs2pid() { + return ws2pid; + } + + public void setWs2pid(String ws2pid) { + this.ws2pid = ws2pid; + } + + public String getSoftware() { + return software; + } + + public void setSoftware(String software) { + this.software = software; + } + + public String getSoftwareVersion() { + return softwareVersion; + } + + public void setSoftwareVersion(String softwareVersion) { + this.softwareVersion = softwareVersion; + } + + public String getPowPrefix() { + return powPrefix; + } + + public void setPowPrefix(String powPrefix) { + this.powPrefix = powPrefix; + } + + public AccessConfig getPrivateConfig() { + return privateConfig; + } + + public void setPrivateConfig(AccessConfig privateConfig) { + this.privateConfig = privateConfig; + } + + public AccessConfig getPublicConfig() { + return publicConfig; + } + + public void setPublicConfig(AccessConfig publicConfig) { + this.publicConfig = publicConfig; + } + + public String getSignature() { + return signature; + } + + public void setSignature(String signature) { + this.signature = signature; + } + + @Override + public String toString() { + return Joiner.on(':').skipNulls().join(new Object[]{ + getPrefix(), "HEAD", version, pubkey, block, ws2pid, software, softwareVersion, powPrefix + }); + } + + @JsonIgnore + protected String getPrefix() { + StringBuilder sb = new StringBuilder(); + sb.append("WS2P"); + + // Private access + if (getPrivateConfig() != null) { + sb.append("O"); + if (getPrivateConfig().isUseTor()) { + sb.append("T"); + } else { + sb.append("C"); + } + + if (getPrivateConfig().getMode() != null) { + switch (getPrivateConfig().getMode()) { + case "all": + sb.append("A"); + break; + case "mixed": + sb.append("M"); + break; + case "strict": + sb.append("S"); + break; + } + } + } + + // Public access + if (getPublicConfig() != null) { + sb.append("I"); + + if (getPublicConfig().isUseTor()) { + sb.append("T"); + } + else { + sb.append("C"); + } + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Ws2pHeads.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Ws2pHeads.java new file mode 100644 index 0000000000000000000000000000000000000000..ebb722a1b1b0aea064219573109de5dd642c723c --- /dev/null +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/Ws2pHeads.java @@ -0,0 +1,111 @@ +package org.duniter.core.client.model.bma; + +import org.duniter.core.client.model.bma.jackson.Ws2pHeadDeserializer; +import org.duniter.core.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Ws2pHeads { + + private static final Logger log = LoggerFactory.getLogger(Ws2pHeads.class); + + public static final String WS2P_PREFIX = "^WS2P(?:O([CT][SAM]))?(?:I([CT]))?$"; + + public static final Pattern WS2P_PREFIX_PATTERN = Pattern.compile(WS2P_PREFIX); + + private Ws2pHeads() { + // helper class + } + + public static Ws2pHead parse(String message) throws IOException { + + try { + String[] parts = message.split(":"); + if (parts.length < 3 || !parts[0].startsWith("WS2P")) { + throw new IOException("Invalid WS2P message format: " + message); + } + // Head message + if ("HEAD".equals(parts[1])) { + if (parts.length < 4) { + throw new IllegalArgumentException("Invalid WS2P message format: " + message); + } + + // Duniter version < 1.6.9 + if (parts.length == 4) { + Ws2pHead result = new Ws2pHead(); + result.setPubkey(parts[2]); + result.setBlock(parts[3]); + } else { + int version = Integer.parseInt(parts[2]); + if (version >= 1) { + Ws2pHead result = new Ws2pHead(); + String prefix = parts[0]; + + // Private/public options + if (prefix.length() > 4) { + + Matcher matches = WS2P_PREFIX_PATTERN.matcher(prefix); + if (!matches.matches()) { + throw new IllegalArgumentException("Invalid WS2P message format: " + message); + } + + // Private options + String privateOptions = matches.group(1); + if (StringUtils.isNotBlank(privateOptions)) { + Ws2pHead.AccessConfig privateConfig = result.getPrivateConfig(); + privateConfig.setUseTor(privateOptions.startsWith("T")); + String mode = privateOptions.substring(1); + switch (mode) { + case "A": + privateConfig.setMode("all"); + break; + case "M": + privateConfig.setMode("mixed"); + break; + case "S": + privateConfig.setMode("strict"); + break; + } + } + + // Public options + String publicOptions = matches.group(2); + if (StringUtils.isNotBlank(publicOptions)) { + Ws2pHead.AccessConfig publicConfig = result.getPrivateConfig(); + publicConfig.setUseTor(publicOptions.startsWith("T")); + publicConfig.setMode("all"); + } + + // For DEBUG only: + log.debug(String.format("Parsing WS2P prefix {%s} into: private %s, public %s", + prefix, + ((result.getPrivateConfig().isUseTor() ? "TOR " : "" ) + (result.getPrivateConfig().getMode())), + ((result.getPublicConfig().isUseTor() ? "TOR " : "" ) + (result.getPublicConfig().getMode())) + )); + } + + result.setVersion(version); + result.setPubkey(parts[3]); + result.setBlock(parts[4]); + result.setWs2pid(parts[5]); + result.setSoftware(parts[6]); + result.setSoftwareVersion(parts[7]); + result.setPowPrefix(parts[8]); + + return result; + } + } + + } + + return null; + } + catch(Exception e) { + throw new IOException(e.getMessage(), e); + } + } +} 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 0309e327bc5361c3c8ef578e20346f5541b0f6de..73714a8db4742abb754cc0f1a2e63c6dade9a5a2 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 @@ -28,6 +28,8 @@ 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.client.model.bma.NetworkWs2pHeads; +import org.duniter.core.client.model.bma.Ws2pHead; /** * Created by blavenie on 07/12/16. @@ -62,6 +64,8 @@ public abstract class JacksonUtils extends SimpleModule { // Network module.addDeserializer(NetworkPeering.Endpoint.class, new EndpointDeserializer()); module.addSerializer(NetworkPeering.Endpoint.class, new EndpointSerializer()); + module.addDeserializer(Ws2pHead.class, new Ws2pHeadDeserializer()); + module.addSerializer(Ws2pHead.class, new Ws2pHeadSerializer()); objectMapper.registerModule(module); diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/Ws2pHeadDeserializer.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/Ws2pHeadDeserializer.java new file mode 100644 index 0000000000000000000000000000000000000000..537797ce21bddd69befa1a1f7015438e80bca579 --- /dev/null +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/Ws2pHeadDeserializer.java @@ -0,0 +1,68 @@ +package org.duniter.core.client.model.bma.jackson; + +/* + * #%L + * Duniter4j :: Core Client API + * %% + * Copyright (C) 2014 - 2017 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 com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import org.duniter.core.client.model.bma.Endpoints; +import org.duniter.core.client.model.bma.NetworkPeering; +import org.duniter.core.client.model.bma.Ws2pHead; +import org.duniter.core.client.model.bma.Ws2pHeads; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** + * Created by blavenie on 07/12/16. + */ +public class Ws2pHeadDeserializer extends JsonDeserializer<Ws2pHead> { + + private static final Logger log = LoggerFactory.getLogger(Ws2pHeadDeserializer.class); + + private boolean debug; + + public Ws2pHeadDeserializer() { + this.debug = log.isDebugEnabled(); + } + + @Override + public Ws2pHead deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + + String ept = jp.getText(); + + try { + return Ws2pHeads.parse(ept); + } catch(IOException e) { + // Unable to parse endpoint: continue (will skip this endpoint) + if (debug) { + log.warn(e.getMessage(), e); // link the exception + } + else { + log.debug(e.getMessage()); + } + return null; + } + } +} \ No newline at end of file diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/Ws2pHeadSerializer.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/Ws2pHeadSerializer.java new file mode 100644 index 0000000000000000000000000000000000000000..971c2b861a9fa978e24e662a69211b97c667476a --- /dev/null +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/bma/jackson/Ws2pHeadSerializer.java @@ -0,0 +1,64 @@ +package org.duniter.core.client.model.bma.jackson; + +/* + * #%L + * Duniter4j :: Core Client API + * %% + * Copyright (C) 2014 - 2017 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 com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.duniter.core.client.model.bma.NetworkPeering; +import org.duniter.core.client.model.bma.NetworkWs2pHeads; +import org.duniter.core.client.model.bma.Ws2pHead; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** + * Created by blavenie on 17/10/18. + */ +public class Ws2pHeadSerializer extends JsonSerializer<Ws2pHead> { + + private static final Logger log = LoggerFactory.getLogger(Ws2pHeadSerializer.class); + + private boolean debug; + + public Ws2pHeadSerializer() { + this.debug = log.isDebugEnabled(); + } + + @Override + public void serialize(Ws2pHead head, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) { + + try { + jsonGenerator.writeString(head.toString()); + } catch(IOException e) { + // Unable to parse endpoint: continue (will skip this endpoint) + if (debug) { + log.warn(e.getMessage(), e); // link the exception + } + else { + log.debug(e.getMessage()); + } + } + } +} \ No newline at end of file diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/local/Peers.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/local/Peers.java index f7e5e06f4c583563a996ba489ea89bc8db8f7427..3648143ab417e00d83a80bef6e420971ef703b7c 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/local/Peers.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/local/Peers.java @@ -104,7 +104,20 @@ public final class Peers { result.setCurrency(firstEp.getCurrency()); result.setPubkey(firstEp.getPubkey()); - result.setBlock(getBlockStamp(firstEp)); + if (firstEp.getPeering() != null) { + result.setBlock(getPeeringBlockStamp(firstEp)); + result.setSignature(firstEp.getPeering().getSignature()); + result.setVersion(firstEp.getPeering().getVersion()); + } + else { + result.setVersion(Protocol.VERSION); + result.setBlock(getStatsBlockStamp(firstEp)); + result.setSignature(null); + } + + // Default values (not stored yet) + // TODO check if still used by clients + result.setStatusTS(0L); // Compute status (=UP is at least one endpoint is UP) String status = endpoints.stream() @@ -114,10 +127,6 @@ public final class Peers { .orElse(Peer.PeerStatus.DOWN).name(); result.setStatus(status); - // Default values (not stored yet) - result.setVersion(Protocol.VERSION); // TODO: get it from the storage (DB, ES, etc.) ? - result.setStatusTS(0L); // TODO make sure this is used by clients ? - // Compute endpoints list List<NetworkPeering.Endpoint> bmaEps = endpoints.stream() .map(Peers::toBmaEndpoint) @@ -152,7 +161,14 @@ public final class Peers { } - public static String getBlockStamp(final Peer peer) { + public static String getPeeringBlockStamp(final Peer peer) { + return peer.getPeering() != null && + peer.getPeering().getBlockNumber() != null && + peer.getPeering().getBlockHash() != null + ? (peer.getPeering().getBlockNumber() + "-" + peer.getPeering().getBlockHash()) : null; + } + + public static String getStatsBlockStamp(final Peer peer) { return peer.getStats() != null && peer.getStats().getBlockNumber() != null && peer.getStats().getBlockHash() != null diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteService.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteService.java index 7b6c5e6c3861e2b698f599223692c9bce7844f6f..bf7956eb283affe2a68da3c3220d510978f617c7 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteService.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteService.java @@ -23,9 +23,7 @@ package org.duniter.core.client.service.bma; */ import org.duniter.core.beans.Service; -import org.duniter.core.client.model.bma.EndpointApi; -import org.duniter.core.client.model.bma.NetworkPeering; -import org.duniter.core.client.model.bma.NetworkPeers; +import org.duniter.core.client.model.bma.*; import org.duniter.core.client.model.local.Peer; import org.duniter.core.util.websocket.WebsocketClientEndpoint; @@ -44,6 +42,8 @@ public interface NetworkRemoteService extends Service { NetworkPeers.Peer getPeerLeaf(Peer peer, String leaf); + List<Ws2pHead> getWs2pHeads(Peer peer); + List<Peer> findPeers(Peer peer, String status, EndpointApi endpointApi, Integer currentBlockNumber, String currentBlockHash); WebsocketClientEndpoint addPeerListener(String currencyId, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect); diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteServiceImpl.java index 12242217c95d0ea92560eebc7492343f90642085..a7c0e63eb3f24853a511e50919242f8beb4aac26 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteServiceImpl.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/bma/NetworkRemoteServiceImpl.java @@ -30,13 +30,12 @@ import java.util.*; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.Lists; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.message.BasicNameValuePair; -import org.duniter.core.client.model.bma.EndpointApi; -import org.duniter.core.client.model.bma.NetworkPeering; -import org.duniter.core.client.model.bma.NetworkPeers; +import org.duniter.core.client.model.bma.*; import org.duniter.core.client.model.bma.jackson.JacksonUtils; import org.duniter.core.client.model.local.Peer; import org.duniter.core.client.model.local.Wallet; @@ -68,6 +67,10 @@ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements N public static final String URL_WS_PEER = "/ws/peer"; + public static final String URL_WS2P = URL_BASE + "/ws2p"; + + public static final String URL_WS2P_HEADS = URL_WS2P + "/heads"; + public NetworkRemoteServiceImpl() { super(); } @@ -154,6 +157,26 @@ public class NetworkRemoteServiceImpl extends BaseRemoteServiceImpl implements N return result; } + @Override + public List<Ws2pHead> getWs2pHeads(Peer peer) { + Preconditions.checkNotNull(peer); + + NetworkWs2pHeads remoteResult = httpService.executeRequest(peer, URL_WS2P_HEADS, NetworkWs2pHeads.class); + + List<Ws2pHead> result = Lists.newArrayList(); + + for (NetworkWs2pHeads.Head remoteWs2pHead: remoteResult.heads) { + + Ws2pHead head = remoteWs2pHead.getMessage(); + if (head != null) { + head.setSignature(remoteWs2pHead.getSig()); + + result.add(head); + } + } + + return result; + } @Override public WebsocketClientEndpoint addPeerListener(String currencyId, WebsocketClientEndpoint.MessageListener listener, boolean autoReconnect) { diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkService.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkService.java index bf8651dbde8836a3680bc06d33d67201814d6989..c470273e9f62383a9c3199e72dcb17cef1f56445 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkService.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkService.java @@ -91,7 +91,7 @@ public interface NetworkService extends Service { final Filter filter, final Sort sort, final boolean autoreconnect, final ExecutorService executor); - CompletableFuture<List<Peer>> asyncRefreshPeers(final Peer mainPeer, final List<Peer> peers, final ExecutorService pool); + CompletableFuture<List<Peer>> refreshPeersAsync(final Peer mainPeer, final List<Peer> peers, final ExecutorService pool); String getVersion(final Peer peer); } diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkServiceImpl.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkServiceImpl.java index f31cc200c5087a6df956ab414859c63e5f669bc4..d61105851966400ec56f3b8cd9c52c9db4701da7 100644 --- a/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkServiceImpl.java +++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/service/local/NetworkServiceImpl.java @@ -155,44 +155,50 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network log.debug("Loading network peers..."); final ExecutorService pool = (executor != null) ? executor : ForkJoinPool.commonPool(); - CompletableFuture<List<Peer>> peersFuture = CompletableFuture.supplyAsync(() -> loadPeerLeafs(mainPeer, filterEndpoints), pool); - CompletableFuture<Map<String, String>> memberUidsFuture = CompletableFuture.supplyAsync(() -> wotRemoteService.getMembersUids(mainPeer), pool); - return CompletableFuture.allOf( - new CompletableFuture[] {peersFuture, memberUidsFuture}) - .thenComposeAsync(v -> { - final Map<String, String> memberUids = memberUidsFuture.join(); - List<Peer> peers = peersFuture.join(); - - List<CompletableFuture<Peer>> list = peers.stream() - .map(peer -> { - // For if same as main peer, - if (mainPeer.getUrl().equals(peer.getUrl())) { - // Update properties - mainPeer.setPubkey(peer.getPubkey()); - mainPeer.setHash(peer.getHash()); - mainPeer.setCurrency(peer.getCurrency()); - // reuse instance - peer = mainPeer; - } - - // Exclude peer with only a local IPv4 address (or localhost) - else if (InetAddressUtils.isLocalAddress(peer.getHost())) { - return CompletableFuture.<Peer>completedFuture(null); - } - - return asyncRefreshPeer(peer, memberUids, pool); - }) - .collect(Collectors.toList()); - return CompletableFutures.allOfToList(list); - }); + return CompletableFuture.supplyAsync(() -> loadPeerLeafs(mainPeer, filterEndpoints), pool) + .thenApply(peers -> + peers.stream() + .map(peer -> { + // For if same as main peer, + if (mainPeer.getUrl().equals(peer.getUrl())) { + // Update properties + mainPeer.setPubkey(peer.getPubkey()); + mainPeer.setHash(peer.getHash()); + mainPeer.setCurrency(peer.getCurrency()); + // reuse instance + peer = mainPeer; + } + + // Exclude peer with only a local IPv4 address (or localhost) + else if (InetAddressUtils.isLocalAddress(peer.getHost())) { + return null; + } + + return peer; + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()) + ) + .thenCompose(peers -> this.refreshPeersAsync(mainPeer, peers, pool)); } - public CompletableFuture<Peer> asyncRefreshPeer(final Peer peer, final Map<String, String> memberUids, final ExecutorService pool) { + public CompletableFuture<Peer> refreshPeerAsync(final Peer peer, + final Map<String, String> memberUids, + final List<Ws2pHead> ws2pHeads, + final ExecutorService pool) { if (log.isDebugEnabled()) log.debug(String.format("[%s] Refreshing peer status", peer.toString())); - return CompletableFuture.supplyAsync(() -> fillNodeSummary(peer), pool) + // WS2P: refresh using heads + if (Peers.hasWs2pEndpoint(peer)) { + return CompletableFuture.supplyAsync(() -> fillWs2pPeer(peer, memberUids, ws2pHeads), pool); + } + + // BMA or ES_CORE + if (Peers.hasBmaEndpoint(peer) || Peers.hasEsCoreEndpoint(peer)) { + + return CompletableFuture.supplyAsync(() -> fillNodeSummary(peer), pool) .thenApplyAsync(this::fillCurrentBlock) .exceptionally(throwable -> { peer.getStats().setStatus(Peer.PeerStatus.DOWN); @@ -225,18 +231,72 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network peer.getStats().setHardshipLevel(0); return peer; }); + } + + // Unknown API: just return the peer + return CompletableFuture.completedFuture(peer); } + public Peer fillWs2pPeer(final Peer peer, final Map<String, String> memberUids, List<Ws2pHead> ws2pHeads) { + if (log.isDebugEnabled()) log.debug(String.format("[%s] Refreshing WS2P peer status", peer.toString())); - public CompletableFuture<List<Peer>> asyncRefreshPeers(final Peer mainPeer, final List<Peer> peers, final ExecutorService pool) { - return CompletableFuture.supplyAsync(() -> wotRemoteService.getMembersUids(mainPeer), pool) - // Refresh all endpoints - .thenApply(memberUids -> - peers.stream().map(peer -> - asyncRefreshPeer(peer, memberUids, pool)) - .collect(Collectors.toList()) + if (StringUtils.isBlank(peer.getPubkey()) || StringUtils.isBlank(peer.getEpId())) return peer; + + Ws2pHead ws2pHead = ws2pHeads.stream().filter(head -> + peer.getPubkey().equals(head.getPubkey()) + && peer.getEpId().equals(head.getWs2pid() ) + ).findFirst().orElse(null); + + if (ws2pHead != null) { + if (ws2pHead.getBlock() != null) { + String[] blockParts = ws2pHead.getBlock().split("-"); + if (blockParts.length == 2) { + peer.getStats().setBlockNumber(Integer.parseInt(blockParts[0])); + peer.getStats().setBlockHash(blockParts[1]); + } + } + peer.getStats().setSoftware(ws2pHead.getSoftware()); + peer.getStats().setVersion(ws2pHead.getSoftwareVersion()); + } + else { + peer.getStats().setStatus(Peer.PeerStatus.DOWN); + } + + // Set uid + String uid = memberUids.get(peer.getPubkey()); + peer.getStats().setUid(uid); + + if (uid != null) { + // Could not known hardship, so fill 0 if member (=can compute) + peer.getStats().setHardshipLevel(0); + } + else { + peer.getStats().setHardshipLevel(null); + } + + return peer; + } + + public CompletableFuture<List<Peer>> refreshPeersAsync(final Peer mainPeer,final List<Peer> peers, final ExecutorService pool) { + + if (CollectionUtils.isEmpty(peers)) return CompletableFuture.completedFuture(null); + + CompletableFuture<Map<String, String>> memberUidsFuture = CompletableFuture.supplyAsync(() -> wotRemoteService.getMembersUids(mainPeer), pool); + CompletableFuture<List<Ws2pHead>> ws2pHeadsFuture = CompletableFuture.supplyAsync(() -> networkRemoteService.getWs2pHeads(mainPeer), pool); + + return CompletableFuture.allOf(memberUidsFuture, ws2pHeadsFuture) + + // Refresh all endpoints + .thenApply(v -> { + final Map<String, String> memberUids = memberUidsFuture.join(); + final List<Ws2pHead> ws2pHeads = ws2pHeadsFuture.join(); + return peers.stream().map(peer -> + refreshPeerAsync(peer, memberUids, ws2pHeads, pool)) + .collect(Collectors.toList()); + }) .thenCompose(CompletableFutures::allOfToList); + } public List<Peer> fillPeerStatsConsensus(final List<Peer> peers) { @@ -245,7 +305,7 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network final Map<String,Long> peerCountByBuid = peers.stream() .filter(peer -> Peers.isReacheable(peer) && Peers.hasDuniterEndpoint(peer)) .map(Peers::buid) - .filter(b -> b != null) + .filter(Objects::nonNull) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); // Compute main consensus buid @@ -253,7 +313,7 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network .sorted(Comparator.comparing(Map.Entry::getValue, Comparator.reverseOrder())) .findFirst(); - final String mainBuid = maxPeerCountEntry.isPresent() ? maxPeerCountEntry.get().getKey() : null;; + final String mainBuid = maxPeerCountEntry.isPresent() ? maxPeerCountEntry.get().getKey() : null; // Compute total of UP peers final Long peersUpTotal = peerCountByBuid.values().stream().mapToLong(Long::longValue).sum(); @@ -372,17 +432,7 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network final List<Peer> newPeers = new ArrayList<>(); addEndpointsAsPeers(bmaPeer, newPeers, null, filter.filterEndpoints); - CompletableFuture<List<CompletableFuture<Peer>>> jobs = - CompletableFuture.supplyAsync(() -> wotRemoteService.getMembersUids(mainPeer), pool) - - // Refresh all endpoints - .thenApply(memberUids -> - newPeers.stream().map(peer -> - asyncRefreshPeer(peer, memberUids, pool)) - .collect(Collectors.toList()) - ); - - jobs.thenCompose(CompletableFutures::allOfToList) + refreshPeersAsync(mainPeer, newPeers, executor) .thenAccept(refreshedPeers -> { if (CollectionUtils.isEmpty(refreshedPeers)) return; @@ -749,4 +799,6 @@ public class NetworkServiceImpl extends BaseRemoteServiceImpl implements Network }); } } + + } diff --git a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/NetworkRemoteServiceTest.java b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/NetworkRemoteServiceTest.java index 085f9550187e6f6ccfc40a3b9cde7b4a8a1dcd2a..de4c8891bff095c8896c695cf92965f4803a7f0d 100644 --- a/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/NetworkRemoteServiceTest.java +++ b/duniter4j-core-client/src/test/java/org/duniter/core/client/service/bma/NetworkRemoteServiceTest.java @@ -27,6 +27,7 @@ import org.duniter.core.client.TestResource; import org.duniter.core.client.config.Configuration; import org.duniter.core.client.model.bma.EndpointApi; import org.duniter.core.client.model.bma.NetworkPeering; +import org.duniter.core.client.model.bma.Ws2pHead; import org.duniter.core.client.model.local.Peer; import org.duniter.core.client.service.ServiceLocator; import org.junit.Assert; @@ -84,6 +85,20 @@ public class NetworkRemoteServiceTest { Assert.assertTrue(result.size() > 0); } + @Test + public void getWs2pHeads() throws Exception { + + List<Ws2pHead> result = service.getWs2pHeads(peer); + + Assert.assertNotNull(result); + Assert.assertTrue(result.size() > 0); + + // log + //result.stream().forEach(head -> + // System.out.println(head.toString()) + //); + } + /* -- internal methods */ protected Peer createTestPeer() {