Skip to content
Snippets Groups Projects
Commit 96663456 authored by Benoit Lavenier's avatar Benoit Lavenier
Browse files

- Start peer indexation from Duniter network

- new mavn submodule, for command line tool
parent 83c7b70d
Branches
Tags
No related merge requests found
Showing
with 667 additions and 37 deletions
File added
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dnl.utils</groupId>
<artifactId>j-text-utils</artifactId>
<packaging>jar</packaging>
<version>0.3.3</version>
<name>Java Text Utilities</name>
<url>http://code.google.com/p/j-text-utils</url>
<dependencies>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>net.sf.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>2.3</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>14.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>org.jvnet.wagon-svn</groupId>
<artifactId>wagon-svn</artifactId>
<version>1.9</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
<distributionManagement>
<repository>
<uniqueVersion>false</uniqueVersion>
<id>googlecode</id>
<url>svn:https://j-text-utils.googlecode.com/svn/trunk/repo/</url>
</repository>
</distributionManagement>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>duniter4j</artifactId>
<groupId>org.duniter</groupId>
<version>0.9.2-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>duniter4j-cmd</artifactId>
<properties>
<jTextUtilsVersion>0.3.3</jTextUtilsVersion>
</properties>
<repositories>
<repository>
<id>d-maven</id>
<url>https://github.com/neilpanchal/j-text-utils/tree/master/repo</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.duniter</groupId>
<artifactId>duniter4j-core-client</artifactId>
<version>${project.version}</version>
</dependency>
<!-- logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</dependency>
<dependency>
<groupId>com.beust</groupId>
<artifactId>jcommander</artifactId>
<version>1.60</version>
</dependency>
<dependency>
<groupId>dnl.utils</groupId>
<artifactId>j-text-utils</artifactId>
<version>${jTextUtilsVersion}</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
<profiles>
<profile>
<id>install-missing-libs</id>
<activation>
<file>
<missing>${settings.localRepository}/dnl/utils/j-text-utils/${jTextUtilsVersion}/j-text-utils-${jTextUtilsVersion}.jar</missing>
</file>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
<executions>
<execution>
<id>installing j-text-utils.jar</id>
<phase>initialize</phase>
<goals>
<goal>install-file</goal>
</goals>
<configuration>
<groupId>dnl.utils</groupId>
<artifactId>j-text-utils</artifactId>
<version>${jTextUtilsVersion}</version>
<packaging>jar</packaging>
<file>${project.basedir}/lib/j-text-utils-${jTextUtilsVersion}.jar</file>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>enforce-dependencies-exists</id>
<phase>generate-sources</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<condition property="displayMessage">
<and>
<not><available file="${project.basedir}/.maven/install.log" /></not>
<!-- do not failed here if performRelease -->
<isfalse value="${performRelease}" />
</and>
</condition>
<property name="installSuccessMessage">*
*************************************************************************
*
* IMPORTANT:
*
* Missing lib dependencies successfully installed on [${settings.localRepository}]
* You should now re-run the build.
* This message will NOT appear again
*
*************************************************************************
</property>
<echo file="${project.basedir}/.maven/install.log">${installSuccessMessage}</echo>
<fail message="${installSuccessMessage}" >
<condition>
<istrue value="${displayMessage}"/>
</condition>
</fail>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>use-installed-libs</id>
<activation>
<file>
<exists>${settings.localRepository}/dnl/utils/j-text-utils/${jTextUtilsVersion}/j-text-utils-${jTextUtilsVersion}.jar</exists>
</file>
</activation>
<dependencies>
<dependency>
<groupId>dnl.utils</groupId>
<artifactId>j-text-utils</artifactId>
<version>${jTextUtilsVersion}</version>
</dependency>
</dependencies>
</profile>
</profiles>
</project>
\ No newline at end of file
package fr.duniter.cmd;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.google.common.collect.Lists;
import fr.duniter.cmd.actions.NetworkAction;
import fr.duniter.cmd.actions.SentMoneyAction;
import org.apache.commons.io.FileUtils;
import org.duniter.core.client.config.Configuration;
import org.duniter.core.client.service.ServiceLocator;
import org.duniter.core.util.StringUtils;
import org.nuiton.i18n.I18n;
import org.nuiton.i18n.init.DefaultI18nInitializer;
import org.nuiton.i18n.init.UserI18nInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.*;
/**
* Created by blavenie on 22/03/17.
*/
public class Main {
@Parameter(names = "-debug", description = "Debug mode", arity = 1)
private boolean debug = false;
@Parameter(names = "--help", help = true)
private boolean help;
@Parameter(names = "-config", description = "Configuration file path")
private String configFilename = "duniter-cmd.config";
public static void main(String ... args) {
Main main = new Main();
main.run(args);
}
protected void run(String ... args) {
Map<String, Runnable> actions = new HashMap<>();
actions.put("network", new NetworkAction());
actions.put("send", new SentMoneyAction());
// Parsing args
JCommander jc = new JCommander(this);
actions.entrySet().stream().forEach(entry -> jc.addCommand(entry.getKey(), entry.getValue()));
try {
jc.parse(args);
}
catch(ParameterException e) {
System.err.println(e.getMessage());
System.err.println("Try --help for usage");
//jc.usage();
System.exit(-1);
}
// Usage, if help or no command
String actionName = jc.getParsedCommand();
if (StringUtils.isBlank(actionName)) {
jc.usage();
// Return error code, if not help
if (!help) System.exit(-1);
return;
}
// Set log level
// TODO
// Init configuration
initConfiguration(configFilename);
// Init i18n
try {
initI18n();
} catch(IOException e) {
System.out.println("Unable to initialize translations");
System.exit(-1);
}
// Set a default account id, then load cache
ServiceLocator.instance().getDataContext().setAccountId(0);
// Initialize service locator
ServiceLocator.instance().init();
Runnable action = actions.get(actionName);
action.run();
}
protected String getI18nBundleName() {
return "duniter4j-core-client-i18n";
}
/* -- -- */
/**
* Convenience methods that could be override to initialize other configuration
*
* @param configFilename
* @param configArgs
*/
protected void initConfiguration(String configFilename) {
String[] configArgs = getConfigArgs();
Configuration config = new Configuration(configFilename, configArgs);
Configuration.setInstance(config);
}
protected void initI18n() throws IOException {
Configuration config = Configuration.instance();
// --------------------------------------------------------------------//
// init i18n
// --------------------------------------------------------------------//
File i18nDirectory = new File(config.getDataDirectory(), "i18n");
if (i18nDirectory.exists()) {
// clean i18n cache
FileUtils.cleanDirectory(i18nDirectory);
}
FileUtils.forceMkdir(i18nDirectory);
if (debug) {
System.out.println("I18N directory: " + i18nDirectory);
}
Locale i18nLocale = config.getI18nLocale();
if (debug) {
System.out.println(String.format("Starts i18n with locale [%s] at [%s]",
i18nLocale, i18nDirectory));
}
I18n.init(new UserI18nInitializer(
i18nDirectory, new DefaultI18nInitializer(getI18nBundleName())),
i18nLocale);
}
protected String[] getConfigArgs() {
List<String> configArgs = Lists.newArrayList();
/*configArgs.addAll(Lists.newArrayList(
"--option", ConfigurationOption.BASEDIR.getKey(), getResourceDirectory().getAbsolutePath()));*/
return configArgs.toArray(new String[configArgs.size()]);
}
}
package fr.duniter.cmd.actions;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import dnl.utils.text.table.TextTable;
import fr.duniter.cmd.actions.utils.Formatters;
import org.duniter.core.client.model.local.Peer;
import org.duniter.core.client.service.ServiceLocator;
import org.duniter.core.client.service.local.NetworkService;
import org.duniter.core.util.CollectionUtils;
import java.util.List;
import java.util.stream.Collectors;
/**
* Created by blavenie on 22/03/17.
*/
@Parameters(commandDescription = "Display network peers")
public class NetworkAction implements Runnable {
@Parameter(names = "-host", description = "Duniter host")
private String host = "g1.duniter.org";
@Parameter(names = "-port", description = "Duniter port")
private int port = 10901;
@Override
public void run() {
NetworkService service = ServiceLocator.instance().getNetworkService();
Peer mainPeer = Peer.newBuilder().setHost(host).setPort(port).build();
List<Peer> peers = service.getPeers(mainPeer);
if (CollectionUtils.isEmpty(peers)) {
System.out.println("No peers found");
}
else {
String[] columnNames = {
"Uid",
"Pubkey",
"Address",
"Status",
"API",
"Version",
"Difficulty",
"Block #"};
List<Object[]> data = peers.stream().map(peer -> {
boolean isUp = peer.getStats().getStatus() == Peer.PeerStatus.UP;
return new Object[] {
Formatters.formatUid(peer.getStats().getUid()),
Formatters.formatPubkey(peer.getPubkey()),
peer.getHost() + ":" + peer.getPort(),
peer.getStats().getStatus().name(),
isUp && peer.isUseSsl() ? "SSL" : null,
isUp ? peer.getStats().getVersion() : null,
isUp ? peer.getStats().getHardshipLevel() : "Mirror",
isUp ? peer.getStats().getBlockNumber() : null
};
})
.collect(Collectors.toList());
Object[][] rows = new Object[data.size()][];
int i = 0;
for (Object[] row : data) {
rows[i++] = row;
}
TextTable tt = new TextTable(columnNames, rows);
// this adds the numbering on the left
tt.setAddRowNumbering(true);
tt.printTable();
}
}
}
package fr.duniter.cmd.actions;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParametersDelegate;
import fr.duniter.cmd.actions.params.WalletParameters;
import fr.duniter.cmd.actions.utils.Formatters;
import org.duniter.core.client.config.Configuration;
import org.duniter.core.client.model.bma.BlockchainParameters;
import org.duniter.core.client.model.local.Currency;
import org.duniter.core.client.model.local.Peer;
import org.duniter.core.client.model.local.Wallet;
import org.duniter.core.client.service.ServiceLocator;
import org.duniter.core.client.service.bma.BlockchainRemoteService;
import org.duniter.core.client.service.bma.TransactionRemoteService;
import org.duniter.core.service.CryptoService;
import org.duniter.core.util.crypto.CryptoUtils;
import org.duniter.core.util.crypto.KeyPair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Created by blavenie on 22/03/17.
*/
public class SentMoneyAction implements Runnable {
private static final Logger log = LoggerFactory.getLogger(SentMoneyAction.class);
@ParametersDelegate
private WalletParameters walletParams = new WalletParameters();
@Parameter(names = "--amount", description = "Amount", required = true)
public int amount;
@Parameter(names = "--dest", description = "Destination pubkey", required = true)
public String destPubkey;
@Parameter(names = "--comment", description = "TX Comment")
public String comment;
@Override
public void run() {
CryptoService cryptoService = ServiceLocator.instance().getCryptoService();
TransactionRemoteService txService = ServiceLocator.instance().getTransactionRemoteService();
Configuration config = Configuration.instance();
Peer peer = Peer.newBuilder().setHost(config.getNodeHost())
.setPort(config.getNodePort())
.build();
Currency currency = ServiceLocator.instance().getBlockchainRemoteService().getCurrencyFromPeer(peer);
ServiceLocator.instance().getCurrencyService().save(currency);
peer.setCurrencyId(currency.getId());
peer.setCurrency(currency.getCurrencyName());
ServiceLocator.instance().getPeerService().save(peer);
// Compute keypair and wallet
KeyPair keypair = cryptoService.getKeyPair(walletParams.salt, walletParams.password);
Wallet wallet = new Wallet(
currency.getCurrencyName(),
null,
keypair.getPubKey(),
keypair.getSecKey());
wallet.setCurrencyId(currency.getId());
System.out.println("Connected to wallet: " + wallet.getPubKeyHash());
txService.transfer(wallet, destPubkey, amount, comment);
System.out.println(String.format("Successfully sent [%d %s] to [%s]",
amount,
Formatters.currencySymbol(currency.getCurrencyName()),
Formatters.formatPubkey(destPubkey)));
}
}
package fr.duniter.cmd.actions.params;
import com.beust.jcommander.Parameter;
/**
* Created by blavenie on 22/03/17.
*/
public class WalletParameters {
@Parameter(names = "--salt", description = "Salt (to generate the keypair)", required = true)
public String salt;
@Parameter(names = "--passwd", description = "Password (to generate the keypair)", required = true)
public String password;
}
package fr.duniter.cmd.actions.utils;
/**
* Created by blavenie on 24/03/17.
*/
public class Formatters {
public static String formatPubkey(String pubkey) {
if (pubkey != null && pubkey.length() > 8) {
return pubkey.substring(0, 8);
}
return pubkey;
}
public static String formatUid(String uid) {
if (uid != null && uid.length() > 20) {
return uid.substring(0, 19);
}
return uid;
}
public static String currencySymbol(String currencyName) {
String[] parts = currencyName.split("-_");
if (parts.length < 2) {
if (currencyName.length() <= 3) {
return currencyName.toUpperCase();
}
else {
return currencyName.toUpperCase().substring(0,1);
}
}
return currencySymbol(parts[0]) + currencySymbol(parts[1]);
}
}
org.duniter.core.client.service.bma.BlockchainRemoteServiceImpl
org.duniter.core.client.service.bma.NetworkRemoteServiceImpl
org.duniter.core.client.service.bma.WotRemoteServiceImpl
org.duniter.core.client.service.bma.TransactionRemoteServiceImpl
org.duniter.core.client.service.elasticsearch.CurrencyRegistryRemoteServiceImpl
org.duniter.core.service.Ed25519CryptoServiceImpl
org.duniter.core.client.service.HttpServiceImpl
org.duniter.core.client.service.DataContext
org.duniter.core.client.service.local.PeerServiceImpl
org.duniter.core.client.service.local.CurrencyServiceImpl
org.duniter.core.client.service.local.NetworkServiceImpl
org.duniter.core.client.dao.mem.MemoryCurrencyDaoImpl
org.duniter.core.client.dao.mem.MemoryPeerDaoImpl
\ No newline at end of file
duniter4j.node.host=192.168.0.5
duniter4j.node.port=10901
duniter4j.node.elasticsearch.host=localhost
duniter4j.node.elasticsearch.port=9200
###
# Global logging configuration
log4j.rootLogger=ERROR, stdout, file
# Console output
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ISO8601} %5p - %m%n
# duniter4j levels
log4j.logger.org.duniter=INFO
log4j.logger.org.duniter.cmd=INFO
#log4j.logger.org.duniter.core.client.service=DEBUG
log4j.logger.org.duniter.core.client.service.local=DEBUG
#log4j.logger.org.duniter.core.client.service.bma=DEBUG
log4j.logger.org.duniter.core.beans=WARN
#log4j.logger.org.duniter.core.client.service=TRACE
log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.file=ucoin-client.log
log4j.appender.file.MaxFileSize=10MB
log4j.appender.file.MaxBackupIndex=4
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{ISO8601} %5p %c - %m%n
......@@ -84,14 +84,14 @@ public class Configuration {
this.applicationConfig.setEncoding(Charsets.UTF_8.name());
this.applicationConfig.setConfigFileName(file);
// get all config providers
// get allOfToList config providers
Set<ApplicationConfigProvider> providers =
ApplicationConfigHelper.getProviders(null,
null,
null,
true);
// load all default options
// load allOfToList default options
ApplicationConfigHelper.loadAllDefaultOption(applicationConfig,
providers);
......@@ -106,7 +106,7 @@ public class Configuration {
// Override application version
initVersion(applicationConfig);
// get all transient and final option keys
// get allOfToList transient and final option keys
Set<String> optionToSkip =
ApplicationConfigHelper.getTransientOptionKeys(providers);
......@@ -234,10 +234,6 @@ public class Configuration {
return applicationConfig.getOption(ConfigurationOption.NODE_CURRENCY.getKey());
}
public String getNodeProtocol() {
return applicationConfig.getOption(ConfigurationOption.NODE_PROTOCOL.getKey());
}
public String getNodeHost() {
return applicationConfig.getOption(ConfigurationOption.NODE_HOST.getKey());
}
......
......@@ -143,14 +143,14 @@ public enum ConfigurationOption implements ConfigOptionDef {
NODE_HOST(
"duniter4j.node.host",
n("duniter4j.config.option.node.host.description"),
"cgeek.fr",
"g1.duniter.org",
String.class,
false),
NODE_PORT(
"duniter4j.node.port",
n("duniter4j.config.option.node.port.description"),
"9330",
"10901",
Integer.class,
false),
......@@ -164,7 +164,7 @@ public enum ConfigurationOption implements ConfigOptionDef {
NETWORK_TIMEOUT(
"duniter4j.network.timeout",
n("duniter4j.config.option.network.timeout.description"),
"100000", // = 10 s
"20000", // = 2 s
Integer.class,
false),
......
......@@ -32,4 +32,13 @@ public interface Constants {
String CURRENCY_NAME = "[A-Za-z0-9_-]";
String PUBKEY = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}";
}
interface HttpStatus {
int SC_TOO_MANY_REQUESTS = 429;
}
interface Config {
int TOO_MANY_REQUEST_RETRY_TIME = 500; // 500 ms
int MAX_SAME_REQUEST_COUNT = 5; // 5 requests before to get 429 error
}
}
......@@ -23,7 +23,10 @@ package org.duniter.core.client.model.bma;
*/
public enum EndpointProtocol {
public enum EndpointApi {
BASIC_MERKLED_API,
BMAS,
ES_CORE_API,
ES_USER_API,
UNDEFINED
}
......@@ -35,6 +35,7 @@ public class NetworkPeering implements Serializable {
private String block;
private String signature;
private String raw;
private String pubkey;
......@@ -112,26 +113,26 @@ public class NetworkPeering implements Serializable {
}
public static class Endpoint implements Serializable {
public EndpointProtocol protocol;
public String url;
public EndpointApi api;
public String dns;
public String ipv4;
public String ipv6;
public Integer port;
public EndpointProtocol getProtocol() {
return protocol;
public EndpointApi getApi() {
return api;
}
public void setProtocol(EndpointProtocol protocol) {
this.protocol = protocol;
public void setApi(EndpointApi api) {
this.api = api;
}
public String getUrl() {
return url;
public String getDns() {
return dns;
}
public void setUrl(String url) {
this.url = url;
public void setDns(String dns) {
this.dns = dns;
}
public String getIpv4() {
......@@ -160,8 +161,8 @@ public class NetworkPeering implements Serializable {
@Override
public String toString() {
String s = "protocol=" + protocol.name() + "\n" +
"url=" + url + "\n" +
String s = "api=" + api.name() + "\n" +
"dns=" + dns + "\n" +
"ipv4=" + ipv4 + "\n" +
"ipv6=" + ipv6 + "\n" +
"port=" + port + "\n";
......
......@@ -148,8 +148,10 @@ public class NetworkPeers implements Serializable {
"status=" + status + "\n" +
"block=" + block + "\n";
for(NetworkPeering.Endpoint endpoint: endpoints) {
if (endpoint != null) {
s += endpoint.toString() + "\n";
}
}
return s;
}
}
......
......@@ -27,9 +27,9 @@ package org.duniter.core.client.model.bma;
*/
public interface Protocol {
String VERSION = "2";
String VERSION = "10";
String TX_VERSION = "3";
String TX_VERSION = "10";
String TYPE_IDENTITY = "Identity";
......
......@@ -89,7 +89,7 @@ public class TxSource {
}
/**
* Source type : <ul>
* Source sortType : <ul>
* <li><code>D</code> : Universal Dividend</li>
* <li><code>T</code> : Transaction</li>
* </ul>
......
......@@ -25,9 +25,9 @@ package org.duniter.core.client.model.bma.gson;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import org.duniter.core.client.model.bma.EndpointProtocol;
import org.duniter.core.client.model.bma.EndpointApi;
import org.duniter.core.client.model.bma.NetworkPeering;
import org.apache.http.conn.util.InetAddressUtils;
import org.duniter.core.util.http.InetAddressUtils;
import java.io.IOException;
import java.util.ArrayList;
......@@ -51,19 +51,19 @@ public class EndpointAdapter extends TypeAdapter<NetworkPeering.Endpoint> {
endpoint.ipv4 = word;
} else if (InetAddressUtils.isIPv6Address(word)) {
endpoint.ipv6 = word;
} else if (word.startsWith("http")) {
endpoint.url = word;
} else if (word.trim().length() > 0) {
endpoint.dns = word;
} else {
try {
endpoint.protocol = EndpointProtocol.valueOf(word);
endpoint.api = EndpointApi.valueOf(word);
} catch (IllegalArgumentException e) {
// skip this part
}
}
}
if (endpoint.protocol == null) {
endpoint.protocol = EndpointProtocol.UNDEFINED;
if (endpoint.api == null) {
endpoint.api = EndpointApi.UNDEFINED;
}
return endpoint;
......@@ -74,8 +74,8 @@ public class EndpointAdapter extends TypeAdapter<NetworkPeering.Endpoint> {
writer.nullValue();
return;
}
writer.value(endpoint.protocol.name() + " " +
endpoint.url + " " +
writer.value(endpoint.api.name() + " " +
endpoint.dns + " " +
endpoint.ipv4 + " " +
endpoint.ipv6 + " " +
endpoint.port);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment