From ea88cd74dbf212bad54e658158d58cc37a52e2fc Mon Sep 17 00:00:00 2001 From: Antoine Toulme Date: Sat, 15 Oct 2022 19:05:57 -0700 Subject: [PATCH 1/2] move discovery to kotlin --- .../scuttlebutt/discovery/LocalIdentity.java | 124 ------------ .../ScuttlebuttLocalDiscoveryService.java | 189 ------------------ .../scuttlebutt/discovery/package-info.java | 25 --- .../scuttlebutt/discovery/LocalIdentity.kt | 100 +++++++++ .../ScuttlebuttLocalDiscoveryService.kt | 177 ++++++++++++++++ 5 files changed, 277 insertions(+), 338 deletions(-) delete mode 100644 scuttlebutt-discovery/src/main/java/org/apache/tuweni/scuttlebutt/discovery/LocalIdentity.java delete mode 100644 scuttlebutt-discovery/src/main/java/org/apache/tuweni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryService.java delete mode 100644 scuttlebutt-discovery/src/main/java/org/apache/tuweni/scuttlebutt/discovery/package-info.java create mode 100644 scuttlebutt-discovery/src/main/kotlin/org/apache/tuweni/scuttlebutt/discovery/LocalIdentity.kt create mode 100644 scuttlebutt-discovery/src/main/kotlin/org/apache/tuweni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryService.kt diff --git a/scuttlebutt-discovery/src/main/java/org/apache/tuweni/scuttlebutt/discovery/LocalIdentity.java b/scuttlebutt-discovery/src/main/java/org/apache/tuweni/scuttlebutt/discovery/LocalIdentity.java deleted file mode 100644 index ba3f25a97..000000000 --- a/scuttlebutt-discovery/src/main/java/org/apache/tuweni/scuttlebutt/discovery/LocalIdentity.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE - * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file - * to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ -package org.apache.tuweni.scuttlebutt.discovery; - -import org.apache.tuweni.bytes.Bytes; -import org.apache.tuweni.crypto.sodium.Signature; -import org.apache.tuweni.scuttlebutt.Identity; - -import java.net.InetSocketAddress; -import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import com.google.common.net.InetAddresses; - -/** - * Representation of an identity associated with an IP and port, used for Scuttlebutt local discovery. - *

- * See https://ssbc.github.io/scuttlebutt-protocol-guide/ for a detailed description of this identity. - */ -public final class LocalIdentity { - - private static final Pattern regexpPattern = Pattern.compile("^net:(.*):(.*)~shs:(.*)$"); - - /** - * Create a local identity from a String of the form net:IP address:port~shs:base64 of public key - * - * @param str the String to interpret - * @return the identity or null if the string doesn't match the format. - */ - public static LocalIdentity fromString(String str) { - Matcher result = regexpPattern.matcher(str); - if (!result.matches()) { - return null; - } - return new LocalIdentity( - result.group(1), - result.group(2), - Identity.fromPublicKey(Signature.PublicKey.fromBytes(Bytes.fromBase64String(result.group(3))))); - - } - - private final Identity id; - - private final InetSocketAddress addr; - - /** - * Constructor for a local identity - * - * @param ip the IP address associated with this local identity - * @param port the port associated with this local identity - * @param id the identity - * @throws NumberFormatException if the port does not represent a number - * @throws IllegalArgumentException if the port is not in the valid port range 0-65536 - */ - public LocalIdentity(String ip, String port, Identity id) { - this(ip, Integer.valueOf(port), id); - } - - /** - * Constructor for a local identity - * - * @param ip the IP address associated with this local identity - * @param port the port associated with this local identity - * @param id the identity - * @throws IllegalArgumentException if the port is not in the valid port range 0-65536 - */ - public LocalIdentity(String ip, int port, Identity id) { - this(new InetSocketAddress(ip, port), id); - } - - /** - * Constructor for a local identity - * - * @param addr the address associated with this local identity - * @param id the identity - * @throws NumberFormatException if the port does not represent a number - * @throws IllegalArgumentException if the port is not in the valid port range 0-65536 - */ - public LocalIdentity(InetSocketAddress addr, Identity id) { - InetAddresses.forString(addr.getHostString()); - this.addr = addr; - this.id = id; - } - - /** - * The canonical form of an invite - * - * @return the local identity in canonical form according to the Scuttlebutt protocol guide. - */ - public String toCanonicalForm() { - return "net:" + addr.getHostString() + ":" + addr.getPort() + "~shs:" + id.publicKeyAsBase64String(); - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - LocalIdentity that = (LocalIdentity) o; - return toCanonicalForm().equals(that.toCanonicalForm()); - } - - @Override - public int hashCode() { - return Objects.hash(id, addr); - } - - @Override - public String toString() { - return toCanonicalForm(); - } -} diff --git a/scuttlebutt-discovery/src/main/java/org/apache/tuweni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryService.java b/scuttlebutt-discovery/src/main/java/org/apache/tuweni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryService.java deleted file mode 100644 index 137594eaf..000000000 --- a/scuttlebutt-discovery/src/main/java/org/apache/tuweni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryService.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE - * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file - * to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ -package org.apache.tuweni.scuttlebutt.discovery; - -import org.apache.tuweni.concurrent.AsyncCompletion; -import org.apache.tuweni.concurrent.CompletableAsyncCompletion; - -import java.net.InetAddress; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; - -import com.google.common.net.InetAddresses; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.datagram.DatagramPacket; -import io.vertx.core.datagram.DatagramSocket; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Scuttlebutt local discovery service, based on the Scuttlebutt network protocol defined - * here. - * - * This service offers two functions: - *

- * It broadcasts to the local network every minute Scuttlebutt identities, as individual packets. - *

- * It listens to broadcasted packets on the local network and relays Scuttlebutt identities identified to listeners. - * - * - * - */ -public class ScuttlebuttLocalDiscoveryService { - private final static Logger logger = LoggerFactory.getLogger(ScuttlebuttLocalDiscoveryService.class); - - private final AtomicBoolean started = new AtomicBoolean(false); - private final Vertx vertx; - private final List> listeners = new ArrayList<>(); - private final List identities = new ArrayList<>(); - private final int listenPort; - private final int broadcastPort; - private final String listenNetworkInterface; - private final String multicastAddress; - - private DatagramSocket udpSocket; - private long timerId; - - /** - * Default constructor. - * - * @param vertx Vert.x instance used to create the UDP socket - * @param listenPort the port to bind the UDP socket to - * @param listenNetworkInterface the network interface to bind the UDP socket to - * @param multicastAddress the address to broadcast multicast packets to - */ - public ScuttlebuttLocalDiscoveryService( - Vertx vertx, - int listenPort, - String listenNetworkInterface, - String multicastAddress) { - this(vertx, listenPort, listenPort, listenNetworkInterface, multicastAddress, true); - } - - - ScuttlebuttLocalDiscoveryService( - Vertx vertx, - int listenPort, - int broadcastPort, - String listenNetworkInterface, - String multicastAddress, - boolean validateMulticast) { - if (validateMulticast) { - InetAddress multicastIP = InetAddresses.forString(multicastAddress); - if (!multicastIP.isMulticastAddress()) { - throw new IllegalArgumentException("Multicast address required, got " + multicastAddress); - } - } - this.vertx = vertx; - this.listenPort = listenPort; - this.broadcastPort = broadcastPort; - this.listenNetworkInterface = listenNetworkInterface; - this.multicastAddress = multicastAddress; - } - - /** - * Starts the service. - * - * @return a handle to track the completion of the operation - */ - public AsyncCompletion start() { - if (started.compareAndSet(false, true)) { - CompletableAsyncCompletion started = AsyncCompletion.incomplete(); - udpSocket = vertx.createDatagramSocket(); - udpSocket.handler(this::listen).listen(listenPort, listenNetworkInterface, handler -> { - if (handler.failed()) { - started.completeExceptionally(handler.cause()); - } else { - started.complete(); - } - }); - timerId = vertx.setPeriodic(60000, (time) -> broadcast()); - return started; - } - return AsyncCompletion.completed(); - } - - void listen(DatagramPacket datagramPacket) { - logger.debug("Received new packet from {}", datagramPacket.sender()); - Buffer buffer = datagramPacket.data(); - if (buffer.length() > 100) { - logger.debug("Packet too long, disregard"); - return; - } - String packetString = buffer.toString(); - try { - LocalIdentity id = LocalIdentity.fromString(packetString); - for (Consumer listener : listeners) { - listener.accept(id); - } - } catch (IllegalArgumentException e) { - logger.debug("Invalid identity payload {}", packetString); - } - } - - void broadcast() { - for (LocalIdentity id : identities) { - udpSocket.send(id.toCanonicalForm(), broadcastPort, multicastAddress, res -> { - if (res.failed()) { - logger.error(res.cause().getMessage(), res.cause()); - } - }); - } - } - - /** - * Stops the service. - * - * @return a handle to track the completion of the operation - */ - public AsyncCompletion stop() { - if (started.compareAndSet(true, false)) { - vertx.cancelTimer(timerId); - CompletableAsyncCompletion result = AsyncCompletion.incomplete(); - udpSocket.close((handler) -> { - if (handler.failed()) { - result.completeExceptionally(handler.cause()); - } else { - result.complete(); - } - }); - return result; - } - return AsyncCompletion.completed(); - } - - /** - * Adds an identity to the ones to be broadcast by the service. - * - * Identities may be added at any time during the lifecycle of the service - * - * @param identity the identity to add to the broadcast list - */ - public void addIdentityToBroadcastList(LocalIdentity identity) { - identities.add(identity); - } - - /** - * Adds a listener to be notified when the service receives UDP packets that match Scuttlebutt identities. - * - * Listeners may be added at any time during the lifecycle of the service - * - * @param listener the listener to add - */ - public void addListener(Consumer listener) { - this.listeners.add(listener); - } -} diff --git a/scuttlebutt-discovery/src/main/java/org/apache/tuweni/scuttlebutt/discovery/package-info.java b/scuttlebutt-discovery/src/main/java/org/apache/tuweni/scuttlebutt/discovery/package-info.java deleted file mode 100644 index 8d5b84612..000000000 --- a/scuttlebutt-discovery/src/main/java/org/apache/tuweni/scuttlebutt/discovery/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE - * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file - * to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - * - * Scuttlebutt discovery service. - * - * This library implements the local discovery service of the Secure Scuttlebutt protocol as defined in - * the Scuttlebutt protocol guide. - * - *

- * These classes are included in the standard Tuweni distribution, or separately when using the gradle dependency - * 'org.apache.tuweni:tuweni-scuttlebutt-discovery' (tuweni-scuttlebutt-discovery.jar). - */ -@ParametersAreNonnullByDefault -package org.apache.tuweni.scuttlebutt.discovery; - -import javax.annotation.ParametersAreNonnullByDefault; diff --git a/scuttlebutt-discovery/src/main/kotlin/org/apache/tuweni/scuttlebutt/discovery/LocalIdentity.kt b/scuttlebutt-discovery/src/main/kotlin/org/apache/tuweni/scuttlebutt/discovery/LocalIdentity.kt new file mode 100644 index 000000000..cf3d24c81 --- /dev/null +++ b/scuttlebutt-discovery/src/main/kotlin/org/apache/tuweni/scuttlebutt/discovery/LocalIdentity.kt @@ -0,0 +1,100 @@ +package org.apache.tuweni.scuttlebutt.discovery + +import com.google.common.net.InetAddresses +import org.apache.tuweni.bytes.Bytes +import org.apache.tuweni.crypto.sodium.Signature +import org.apache.tuweni.scuttlebutt.Identity +import java.net.InetSocketAddress +import java.util.Objects +import java.util.regex.Pattern + +/** + * Representation of an identity associated with an IP and port, used for Scuttlebutt local discovery. + * + * + * See https://ssbc.github.io/scuttlebutt-protocol-guide/ for a detailed description of this identity. + */ +class LocalIdentity(addr: InetSocketAddress, id: Identity) { + private val id: Identity + private val addr: InetSocketAddress + + /** + * Constructor for a local identity + * + * @param ip the IP address associated with this local identity + * @param port the port associated with this local identity + * @param id the identity + * @throws NumberFormatException if the port does not represent a number + * @throws IllegalArgumentException if the port is not in the valid port range 0-65536 + */ + constructor(ip: String?, port: String?, id: Identity) : this(ip, Integer.valueOf(port), id) {} + + /** + * Constructor for a local identity + * + * @param ip the IP address associated with this local identity + * @param port the port associated with this local identity + * @param id the identity + * @throws IllegalArgumentException if the port is not in the valid port range 0-65536 + */ + constructor(ip: String?, port: Int, id: Identity) : this(InetSocketAddress(ip, port), id) {} + + /** + * Constructor for a local identity + * + * @param addr the address associated with this local identity + * @param id the identity + * @throws NumberFormatException if the port does not represent a number + * @throws IllegalArgumentException if the port is not in the valid port range 0-65536 + */ + init { + InetAddresses.forString(addr.hostString) + this.addr = addr + this.id = id + } + + /** + * The canonical form of an invite + * + * @return the local identity in canonical form according to the Scuttlebutt protocol guide. + */ + fun toCanonicalForm(): String { + return "net:" + addr.hostString + ":" + addr.port + "~shs:" + id.publicKeyAsBase64String() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val that = other as LocalIdentity + return toCanonicalForm() == that.toCanonicalForm() + } + + override fun hashCode(): Int { + return Objects.hash(id, addr) + } + + override fun toString(): String { + return toCanonicalForm() + } + + companion object { + private val regexpPattern = Pattern.compile("^net:(.*):(.*)~shs:(.*)$") + + /** + * Create a local identity from a String of the form net:IP address:port~shs:base64 of public key + * + * @param str the String to interpret + * @return the identity or null if the string doesn't match the format. + */ + fun fromString(str: String?): LocalIdentity? { + val result = regexpPattern.matcher(str) + return if (!result.matches()) { + null + } else LocalIdentity( + result.group(1), + result.group(2), + Identity.fromPublicKey(Signature.PublicKey.fromBytes(Bytes.fromBase64String(result.group(3)))) + ) + } + } +} diff --git a/scuttlebutt-discovery/src/main/kotlin/org/apache/tuweni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryService.kt b/scuttlebutt-discovery/src/main/kotlin/org/apache/tuweni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryService.kt new file mode 100644 index 000000000..38879f0a8 --- /dev/null +++ b/scuttlebutt-discovery/src/main/kotlin/org/apache/tuweni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryService.kt @@ -0,0 +1,177 @@ +package org.apache.tuweni.scuttlebutt.discovery + +import com.google.common.net.InetAddresses +import io.vertx.core.AsyncResult +import io.vertx.core.Handler +import io.vertx.core.Vertx +import io.vertx.core.datagram.DatagramPacket +import io.vertx.core.datagram.DatagramSocket +import org.apache.tuweni.concurrent.AsyncCompletion +import org.slf4j.LoggerFactory +import java.util.concurrent.atomic.AtomicBoolean +import java.util.function.Consumer + +/** + * Scuttlebutt local discovery service, based on the Scuttlebutt network protocol defined + * [here](https://ssbc.github.io/scuttlebutt-protocol-guide/). + * + * This service offers two functions: + * + * It broadcasts to the local network every minute Scuttlebutt identities, as individual packets. + * + * It listens to broadcasted packets on the local network and relays Scuttlebutt identities identified to listeners. + * + */ +class ScuttlebuttLocalDiscoveryService internal constructor( + vertx: Vertx, + listenPort: Int, + broadcastPort: Int, + listenNetworkInterface: String?, + multicastAddress: String, + validateMulticast: Boolean, +) { + + companion object { + private val logger = LoggerFactory.getLogger(ScuttlebuttLocalDiscoveryService::class.java) + } + + private val started = AtomicBoolean(false) + private val vertx: Vertx + private val listeners: MutableList> = ArrayList() + private val identities: MutableList = ArrayList() + private val listenPort: Int + private val broadcastPort: Int + private val listenNetworkInterface: String? + private val multicastAddress: String + private var udpSocket: DatagramSocket? = null + private var timerId: Long = 0 + + /** + * Default constructor. + * + * @param vertx Vert.x instance used to create the UDP socket + * @param listenPort the port to bind the UDP socket to + * @param listenNetworkInterface the network interface to bind the UDP socket to + * @param multicastAddress the address to broadcast multicast packets to + */ + constructor( + vertx: Vertx, + listenPort: Int, + listenNetworkInterface: String?, + multicastAddress: String, + ) : this(vertx, listenPort, listenPort, listenNetworkInterface, multicastAddress, true) { + } + + init { + if (validateMulticast) { + val multicastIP = InetAddresses.forString(multicastAddress) + require(multicastIP.isMulticastAddress) { "Multicast address required, got $multicastAddress" } + } + this.vertx = vertx + this.listenPort = listenPort + this.broadcastPort = broadcastPort + this.listenNetworkInterface = listenNetworkInterface + this.multicastAddress = multicastAddress + } + + /** + * Starts the service. + * + * @return a handle to track the completion of the operation + */ + fun start(): AsyncCompletion { + if (started.compareAndSet(false, true)) { + val started = AsyncCompletion.incomplete() + udpSocket = vertx.createDatagramSocket() + udpSocket!!.handler { datagramPacket: DatagramPacket -> + listen( + datagramPacket + ) + }.listen( + listenPort, listenNetworkInterface + ) { handler: AsyncResult -> + if (handler.failed()) { + started.completeExceptionally(handler.cause()) + } else { + started.complete() + } + } + timerId = vertx.setPeriodic(60000) { time: Long? -> broadcast() } + return started + } + return AsyncCompletion.completed() + } + + fun listen(datagramPacket: DatagramPacket) { + logger.debug("Received new packet from {}", datagramPacket.sender()) + val buffer = datagramPacket.data() + if (buffer.length() > 100) { + logger.debug("Packet too long, disregard") + return + } + val packetString = buffer.toString() + try { + val id = LocalIdentity.fromString(packetString) + for (listener in listeners) { + listener.accept(id) + } + } catch (e: IllegalArgumentException) { + logger.debug("Invalid identity payload {}", packetString) + } + } + + fun broadcast() { + for (id in identities) { + udpSocket!!.send( + id.toCanonicalForm(), broadcastPort, multicastAddress + ) { res: AsyncResult -> + if (res.failed()) { + logger.error(res.cause().message, res.cause()) + } + } + } + } + + /** + * Stops the service. + * + * @return a handle to track the completion of the operation + */ + fun stop(): AsyncCompletion { + if (started.compareAndSet(true, false)) { + vertx.cancelTimer(timerId) + val result = AsyncCompletion.incomplete() + udpSocket!!.close { handler: AsyncResult -> + if (handler.failed()) { + result.completeExceptionally(handler.cause()) + } else { + result.complete() + } + } + return result + } + return AsyncCompletion.completed() + } + + /** + * Adds an identity to the ones to be broadcast by the service. + * + * Identities may be added at any time during the lifecycle of the service + * + * @param identity the identity to add to the broadcast list + */ + fun addIdentityToBroadcastList(identity: LocalIdentity) { + identities.add(identity) + } + + /** + * Adds a listener to be notified when the service receives UDP packets that match Scuttlebutt identities. + * + * Listeners may be added at any time during the lifecycle of the service + * + * @param listener the listener to add + */ + fun addListener(listener: Consumer) { + listeners.add(listener) + } +} From 1fd174bb90649cd4980abfa7ac3be9261fcdcec7 Mon Sep 17 00:00:00 2001 From: Antoine Toulme Date: Sat, 15 Oct 2022 20:23:01 -0700 Subject: [PATCH 2/2] move discovery code to kotlin --- scuttlebutt-discovery/build.gradle | 2 + .../ScuttlebuttLocalDiscoveryServiceTest.java | 98 --------------- .../ScuttlebuttLocalDiscoveryServiceTest.kt | 119 ++++++++++++++++++ .../scuttlebutt/discovery/LocalIdentity.kt | 87 +++++++------ .../ScuttlebuttLocalDiscoveryService.kt | 97 ++++++-------- .../discovery/LocalIdentityTest.java | 71 ----------- .../discovery/LocalIdentityTest.kt | 73 +++++++++++ 7 files changed, 277 insertions(+), 270 deletions(-) delete mode 100644 scuttlebutt-discovery/src/integrationTest/java/org/apache/tuweni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryServiceTest.java create mode 100644 scuttlebutt-discovery/src/integrationTest/kotlin/org/apache/tueni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryServiceTest.kt delete mode 100644 scuttlebutt-discovery/src/test/java/org/apache/tuweni/scuttlebutt/discovery/LocalIdentityTest.java create mode 100644 scuttlebutt-discovery/src/test/kotlin/org/apache/tuweni/scuttlebutt/discovery/LocalIdentityTest.kt diff --git a/scuttlebutt-discovery/build.gradle b/scuttlebutt-discovery/build.gradle index a8ec4d8c8..8e8c0c41f 100644 --- a/scuttlebutt-discovery/build.gradle +++ b/scuttlebutt-discovery/build.gradle @@ -19,6 +19,8 @@ dependencies { implementation project(':scuttlebutt') implementation 'com.google.guava:guava' implementation 'io.vertx:vertx-core' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core' + implementation 'io.vertx:vertx-lang-kotlin-coroutines' implementation 'org.slf4j:slf4j-api' testImplementation project(':junit') diff --git a/scuttlebutt-discovery/src/integrationTest/java/org/apache/tuweni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryServiceTest.java b/scuttlebutt-discovery/src/integrationTest/java/org/apache/tuweni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryServiceTest.java deleted file mode 100644 index 5942cf662..000000000 --- a/scuttlebutt-discovery/src/integrationTest/java/org/apache/tuweni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryServiceTest.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE - * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file - * to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ -package org.apache.tuweni.scuttlebutt.discovery; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assumptions.assumeTrue; - -import org.apache.tuweni.concurrent.AsyncCompletion; -import org.apache.tuweni.crypto.sodium.Sodium; -import org.apache.tuweni.junit.VertxExtension; -import org.apache.tuweni.junit.VertxInstance; -import org.apache.tuweni.scuttlebutt.Identity; - -import java.util.concurrent.atomic.AtomicReference; - -import io.vertx.core.Vertx; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -@ExtendWith(VertxExtension.class) -class ScuttlebuttLocalDiscoveryServiceTest { - - @BeforeAll - static void checkAvailable() { - assumeTrue(Sodium.isAvailable(), "Sodium native library is not available"); - } - - @Test - void startStop(@VertxInstance Vertx vertx) throws Exception { - ScuttlebuttLocalDiscoveryService service = - new ScuttlebuttLocalDiscoveryService(vertx, 0, "127.0.0.1", "233.0.10.0"); - service.start().join(); - service.stop().join(); - } - - @Test - void startStart(@VertxInstance Vertx vertx) throws Exception { - ScuttlebuttLocalDiscoveryService service = - new ScuttlebuttLocalDiscoveryService(vertx, 0, "127.0.0.1", "233.0.10.0"); - service.start().join(); - service.start().join(); - service.stop().join(); - } - - @Test - void invalidMulticastAddress(@VertxInstance Vertx vertx) throws Exception { - assertThrows( - IllegalArgumentException.class, - () -> new ScuttlebuttLocalDiscoveryService(vertx, 8008, "127.0.0.1", "10.0.0.0")); - } - - @Test - void stopFirst(@VertxInstance Vertx vertx) throws Exception { - ScuttlebuttLocalDiscoveryService service = - new ScuttlebuttLocalDiscoveryService(vertx, 0, "127.0.0.1", "233.0.10.0"); - service.stop().join(); - service.start().join(); - service.stop().join(); - } - - @Test - void broadcastAndListen(@VertxInstance Vertx vertx) throws Exception { - ScuttlebuttLocalDiscoveryService service = - new ScuttlebuttLocalDiscoveryService(vertx, 18008, 18009, "127.0.0.1", "127.0.0.1", false); - ScuttlebuttLocalDiscoveryService service2 = - new ScuttlebuttLocalDiscoveryService(vertx, 18009, 18008, "127.0.0.1", "127.0.0.1", false); - - try { - service2.start().join(); - AtomicReference ref = new AtomicReference<>(); - service2.addListener(ref::set); - - LocalIdentity localId = new LocalIdentity("10.0.0.1", 10000, Identity.random()); - service.addIdentityToBroadcastList(localId); - service.start().join(); - service.broadcast(); - Thread.sleep(1000); - assertNotNull(ref.get()); - assertEquals(localId, ref.get()); - } finally { - AsyncCompletion.allOf(service2.stop(), service.stop()).join(); - } - } - -} diff --git a/scuttlebutt-discovery/src/integrationTest/kotlin/org/apache/tueni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryServiceTest.kt b/scuttlebutt-discovery/src/integrationTest/kotlin/org/apache/tueni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryServiceTest.kt new file mode 100644 index 000000000..a9561ccc5 --- /dev/null +++ b/scuttlebutt-discovery/src/integrationTest/kotlin/org/apache/tueni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryServiceTest.kt @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tueni.scuttlebutt.discovery + +import io.vertx.core.Vertx +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.apache.tuweni.crypto.sodium.Sodium +import org.apache.tuweni.junit.VertxExtension +import org.apache.tuweni.junit.VertxInstance +import org.apache.tuweni.scuttlebutt.Identity +import org.apache.tuweni.scuttlebutt.discovery.LocalIdentity +import org.apache.tuweni.scuttlebutt.discovery.ScuttlebuttLocalDiscoveryService +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assumptions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.util.concurrent.atomic.AtomicReference + +@ExtendWith(VertxExtension::class) +internal class ScuttlebuttLocalDiscoveryServiceTest { + companion object { + @JvmStatic + @BeforeAll + fun checkAvailable() { + Assumptions.assumeTrue(Sodium.isAvailable(), "Sodium native library is not available") + } + } + + @Test + @Throws(Exception::class) + fun startStop(@VertxInstance vertx: Vertx) = runBlocking { + val service = ScuttlebuttLocalDiscoveryService(vertx, 0, 0, "127.0.0.1", "233.0.10.0") + service.start() + service.stop() + } + + @Test + @Throws(Exception::class) + fun startStart(@VertxInstance vertx: Vertx) = runBlocking { + val service = ScuttlebuttLocalDiscoveryService(vertx, 0, 0, "127.0.0.1", "233.0.10.0") + service.start() + service.start() + service.stop() + } + + @Test + @Throws(Exception::class) + fun invalidMulticastAddress(@VertxInstance vertx: Vertx) { + Assertions.assertThrows( + IllegalArgumentException::class.java + ) { + ScuttlebuttLocalDiscoveryService( + vertx, + 8008, 0, + "127.0.0.1", + "10.0.0.0" + ) + } + } + + @Test + @Throws(Exception::class) + fun stopFirst(@VertxInstance vertx: Vertx) = runBlocking { + val service = ScuttlebuttLocalDiscoveryService(vertx, 0, 0, "127.0.0.1", "233.0.10.0") + service.stop() + service.start() + service.stop() + } + + @Test + @Throws(Exception::class) + fun broadcastAndListen(@VertxInstance vertx: Vertx?) = runBlocking { + val service = ScuttlebuttLocalDiscoveryService(vertx!!, 18008, 18009, "127.0.0.1", "127.0.0.1", false) + val service2 = ScuttlebuttLocalDiscoveryService(vertx, 18009, 18008, "127.0.0.1", "127.0.0.1", false) + try { + service2.start() + val ref = AtomicReference() + service2.addListener { newValue: LocalIdentity? -> + ref.set( + newValue + ) + } + val localId = LocalIdentity("10.0.0.1", 10000, Identity.random()) + service.addIdentityToBroadcastList(localId) + service.start() + service.broadcast() + delay(1000) + Assertions.assertNotNull(ref.get()) + Assertions.assertEquals(localId, ref.get()) + } finally { + listOf( + async { + service2.stop() + }, + async { + service.stop() + } + ).awaitAll() + } + } +} diff --git a/scuttlebutt-discovery/src/main/kotlin/org/apache/tuweni/scuttlebutt/discovery/LocalIdentity.kt b/scuttlebutt-discovery/src/main/kotlin/org/apache/tuweni/scuttlebutt/discovery/LocalIdentity.kt index cf3d24c81..734283011 100644 --- a/scuttlebutt-discovery/src/main/kotlin/org/apache/tuweni/scuttlebutt/discovery/LocalIdentity.kt +++ b/scuttlebutt-discovery/src/main/kotlin/org/apache/tuweni/scuttlebutt/discovery/LocalIdentity.kt @@ -1,6 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.apache.tuweni.scuttlebutt.discovery -import com.google.common.net.InetAddresses import org.apache.tuweni.bytes.Bytes import org.apache.tuweni.crypto.sodium.Signature import org.apache.tuweni.scuttlebutt.Identity @@ -13,10 +28,35 @@ import java.util.regex.Pattern * * * See https://ssbc.github.io/scuttlebutt-protocol-guide/ for a detailed description of this identity. + * + * @param addr the address associated with this local identity + * @param id the identity + * @throws NumberFormatException if the port does not represent a number + * @throws IllegalArgumentException if the port is not in the valid port range 0-65536 */ -class LocalIdentity(addr: InetSocketAddress, id: Identity) { - private val id: Identity - private val addr: InetSocketAddress +class LocalIdentity(val addr: InetSocketAddress, val id: Identity) { + + companion object { + private val regexpPattern = Pattern.compile("^net:(.*):(.*)~shs:(.*)$") + + /** + * Create a local identity from a String of the form net:IP address:port~shs:base64 of public key + * + * @param str the String to interpret + * @return the identity or null if the string doesn't match the format. + */ + @JvmStatic + fun fromString(str: String?): LocalIdentity? { + val result = regexpPattern.matcher(str) + return if (!result.matches()) { + null + } else LocalIdentity( + result.group(1), + result.group(2), + Identity.fromPublicKey(Signature.PublicKey.fromBytes(Bytes.fromBase64String(result.group(3)))) + ) + } + } /** * Constructor for a local identity @@ -27,7 +67,7 @@ class LocalIdentity(addr: InetSocketAddress, id: Identity) { * @throws NumberFormatException if the port does not represent a number * @throws IllegalArgumentException if the port is not in the valid port range 0-65536 */ - constructor(ip: String?, port: String?, id: Identity) : this(ip, Integer.valueOf(port), id) {} + constructor(ip: String, port: String, id: Identity) : this(ip, Integer.valueOf(port), id) /** * Constructor for a local identity @@ -37,21 +77,7 @@ class LocalIdentity(addr: InetSocketAddress, id: Identity) { * @param id the identity * @throws IllegalArgumentException if the port is not in the valid port range 0-65536 */ - constructor(ip: String?, port: Int, id: Identity) : this(InetSocketAddress(ip, port), id) {} - - /** - * Constructor for a local identity - * - * @param addr the address associated with this local identity - * @param id the identity - * @throws NumberFormatException if the port does not represent a number - * @throws IllegalArgumentException if the port is not in the valid port range 0-65536 - */ - init { - InetAddresses.forString(addr.hostString) - this.addr = addr - this.id = id - } + constructor(ip: String, port: Int, id: Identity) : this(InetSocketAddress(ip, port), id) /** * The canonical form of an invite @@ -76,25 +102,4 @@ class LocalIdentity(addr: InetSocketAddress, id: Identity) { override fun toString(): String { return toCanonicalForm() } - - companion object { - private val regexpPattern = Pattern.compile("^net:(.*):(.*)~shs:(.*)$") - - /** - * Create a local identity from a String of the form net:IP address:port~shs:base64 of public key - * - * @param str the String to interpret - * @return the identity or null if the string doesn't match the format. - */ - fun fromString(str: String?): LocalIdentity? { - val result = regexpPattern.matcher(str) - return if (!result.matches()) { - null - } else LocalIdentity( - result.group(1), - result.group(2), - Identity.fromPublicKey(Signature.PublicKey.fromBytes(Bytes.fromBase64String(result.group(3)))) - ) - } - } } diff --git a/scuttlebutt-discovery/src/main/kotlin/org/apache/tuweni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryService.kt b/scuttlebutt-discovery/src/main/kotlin/org/apache/tuweni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryService.kt index 38879f0a8..f5f9431de 100644 --- a/scuttlebutt-discovery/src/main/kotlin/org/apache/tuweni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryService.kt +++ b/scuttlebutt-discovery/src/main/kotlin/org/apache/tuweni/scuttlebutt/discovery/ScuttlebuttLocalDiscoveryService.kt @@ -1,12 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.apache.tuweni.scuttlebutt.discovery import com.google.common.net.InetAddresses import io.vertx.core.AsyncResult -import io.vertx.core.Handler import io.vertx.core.Vertx import io.vertx.core.datagram.DatagramPacket import io.vertx.core.datagram.DatagramSocket -import org.apache.tuweni.concurrent.AsyncCompletion +import io.vertx.kotlin.coroutines.await import org.slf4j.LoggerFactory import java.util.concurrent.atomic.AtomicBoolean import java.util.function.Consumer @@ -21,14 +36,20 @@ import java.util.function.Consumer * * It listens to broadcasted packets on the local network and relays Scuttlebutt identities identified to listeners. * + * + * @param vertx Vert.x instance used to create the UDP socket + * @param listenPort the port to bind the UDP socket to + * @param listenNetworkInterface the network interface to bind the UDP socket to + * @param multicastAddress the address to broadcast multicast packets to + * @param validateMulticast validate the multicast address to use - true by default, used for tests. */ -class ScuttlebuttLocalDiscoveryService internal constructor( - vertx: Vertx, - listenPort: Int, - broadcastPort: Int, - listenNetworkInterface: String?, - multicastAddress: String, - validateMulticast: Boolean, +class ScuttlebuttLocalDiscoveryService( + private val vertx: Vertx, + private val listenPort: Int, + private val broadcastPort: Int, + private val listenNetworkInterface: String, + private val multicastAddress: String, + private val validateMulticast: Boolean = true, ) { companion object { @@ -36,42 +57,16 @@ class ScuttlebuttLocalDiscoveryService internal constructor( } private val started = AtomicBoolean(false) - private val vertx: Vertx private val listeners: MutableList> = ArrayList() private val identities: MutableList = ArrayList() - private val listenPort: Int - private val broadcastPort: Int - private val listenNetworkInterface: String? - private val multicastAddress: String private var udpSocket: DatagramSocket? = null private var timerId: Long = 0 - /** - * Default constructor. - * - * @param vertx Vert.x instance used to create the UDP socket - * @param listenPort the port to bind the UDP socket to - * @param listenNetworkInterface the network interface to bind the UDP socket to - * @param multicastAddress the address to broadcast multicast packets to - */ - constructor( - vertx: Vertx, - listenPort: Int, - listenNetworkInterface: String?, - multicastAddress: String, - ) : this(vertx, listenPort, listenPort, listenNetworkInterface, multicastAddress, true) { - } - init { if (validateMulticast) { val multicastIP = InetAddresses.forString(multicastAddress) require(multicastIP.isMulticastAddress) { "Multicast address required, got $multicastAddress" } } - this.vertx = vertx - this.listenPort = listenPort - this.broadcastPort = broadcastPort - this.listenNetworkInterface = listenNetworkInterface - this.multicastAddress = multicastAddress } /** @@ -79,9 +74,8 @@ class ScuttlebuttLocalDiscoveryService internal constructor( * * @return a handle to track the completion of the operation */ - fun start(): AsyncCompletion { + suspend fun start() { if (started.compareAndSet(false, true)) { - val started = AsyncCompletion.incomplete() udpSocket = vertx.createDatagramSocket() udpSocket!!.handler { datagramPacket: DatagramPacket -> listen( @@ -89,20 +83,12 @@ class ScuttlebuttLocalDiscoveryService internal constructor( ) }.listen( listenPort, listenNetworkInterface - ) { handler: AsyncResult -> - if (handler.failed()) { - started.completeExceptionally(handler.cause()) - } else { - started.complete() - } - } - timerId = vertx.setPeriodic(60000) { time: Long? -> broadcast() } - return started + ).await() + timerId = vertx.setPeriodic(60000) { broadcast() } } - return AsyncCompletion.completed() } - fun listen(datagramPacket: DatagramPacket) { + private fun listen(datagramPacket: DatagramPacket) { logger.debug("Received new packet from {}", datagramPacket.sender()) val buffer = datagramPacket.data() if (buffer.length() > 100) { @@ -116,7 +102,7 @@ class ScuttlebuttLocalDiscoveryService internal constructor( listener.accept(id) } } catch (e: IllegalArgumentException) { - logger.debug("Invalid identity payload {}", packetString) + logger.debug("Invalid identity payload {}", packetString, e) } } @@ -137,20 +123,11 @@ class ScuttlebuttLocalDiscoveryService internal constructor( * * @return a handle to track the completion of the operation */ - fun stop(): AsyncCompletion { + suspend fun stop() { if (started.compareAndSet(true, false)) { vertx.cancelTimer(timerId) - val result = AsyncCompletion.incomplete() - udpSocket!!.close { handler: AsyncResult -> - if (handler.failed()) { - result.completeExceptionally(handler.cause()) - } else { - result.complete() - } - } - return result + udpSocket?.close()?.await() } - return AsyncCompletion.completed() } /** diff --git a/scuttlebutt-discovery/src/test/java/org/apache/tuweni/scuttlebutt/discovery/LocalIdentityTest.java b/scuttlebutt-discovery/src/test/java/org/apache/tuweni/scuttlebutt/discovery/LocalIdentityTest.java deleted file mode 100644 index 283882778..000000000 --- a/scuttlebutt-discovery/src/test/java/org/apache/tuweni/scuttlebutt/discovery/LocalIdentityTest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE - * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file - * to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ -package org.apache.tuweni.scuttlebutt.discovery; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assumptions.assumeTrue; - -import org.apache.tuweni.crypto.sodium.Sodium; -import org.apache.tuweni.scuttlebutt.Identity; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -class LocalIdentityTest { - - @BeforeAll - static void checkAvailable() { - assumeTrue(Sodium.isAvailable(), "Sodium native library is not available"); - } - - @Test - void localIdentityInvalidPort() { - assertThrows(IllegalArgumentException.class, () -> { - new LocalIdentity("0.0.0.0", 450000, Identity.random()); - }); - } - - - @Test - void localIdentityInvalidIP() { - assertThrows(IllegalArgumentException.class, () -> { - new LocalIdentity("0.0.0.0a", 45000, Identity.random()); - }); - } - - @Test - void localIdentityCanonicalForm() { - Identity id = Identity.random(); - LocalIdentity localId = new LocalIdentity("0.0.0.0", 45000, id); - assertEquals("net:0.0.0.0:45000~shs:" + id.publicKeyAsBase64String(), localId.toCanonicalForm()); - assertEquals(localId.toCanonicalForm(), localId.toString()); - } - - @Test - void localIdentityFromString() { - Identity id = Identity.random(); - LocalIdentity localId = new LocalIdentity("0.0.0.0", 45000, id); - LocalIdentity fromString = LocalIdentity.fromString("net:0.0.0.0:45000~shs:" + id.publicKeyAsBase64String()); - assertEquals(fromString, localId); - } - - @Test - void malformedIdentity() { - Identity id = Identity.random(); - assertNull(LocalIdentity.fromString("nt:0.0.0.0:45000~shs:" + id.publicKeyAsBase64String())); - assertNull(LocalIdentity.fromString("net:0.0.0.0:45000~ss:" + id.publicKeyAsBase64String())); - assertNull(LocalIdentity.fromString("nt:0.0.0.0:45000~shs:" + id.publicKeyAsBase64String().substring(12))); - } -} diff --git a/scuttlebutt-discovery/src/test/kotlin/org/apache/tuweni/scuttlebutt/discovery/LocalIdentityTest.kt b/scuttlebutt-discovery/src/test/kotlin/org/apache/tuweni/scuttlebutt/discovery/LocalIdentityTest.kt new file mode 100644 index 000000000..bbde9de26 --- /dev/null +++ b/scuttlebutt-discovery/src/test/kotlin/org/apache/tuweni/scuttlebutt/discovery/LocalIdentityTest.kt @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tuweni.scuttlebutt.discovery + +import org.apache.tuweni.crypto.sodium.Sodium +import org.apache.tuweni.scuttlebutt.Identity +import org.apache.tuweni.scuttlebutt.discovery.LocalIdentity.Companion.fromString +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assumptions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +internal class LocalIdentityTest { + + companion object { + @JvmStatic + @BeforeAll + fun checkAvailable() { + Assumptions.assumeTrue(Sodium.isAvailable(), "Sodium native library is not available") + } + } + + @Test + fun localIdentityInvalidPort() { + Assertions.assertThrows( + IllegalArgumentException::class.java + ) { + LocalIdentity( + "0.0.0.0", + 450000, + Identity.random() + ) + } + } + + @Test + fun localIdentityCanonicalForm() { + val id = Identity.random() + val localId = LocalIdentity("0.0.0.0", 45000, id) + Assertions.assertEquals("net:0.0.0.0:45000~shs:" + id.publicKeyAsBase64String(), localId.toCanonicalForm()) + Assertions.assertEquals(localId.toCanonicalForm(), localId.toString()) + } + + @Test + fun localIdentityFromString() { + val id = Identity.random() + val localId = LocalIdentity("0.0.0.0", 45000, id) + val fromString = fromString("net:0.0.0.0:45000~shs:" + id.publicKeyAsBase64String()) + Assertions.assertEquals(fromString, localId) + } + + @Test + fun malformedIdentity() { + val id = Identity.random() + Assertions.assertNull(fromString("nt:0.0.0.0:45000~shs:" + id.publicKeyAsBase64String())) + Assertions.assertNull(fromString("net:0.0.0.0:45000~ss:" + id.publicKeyAsBase64String())) + Assertions.assertNull(fromString("nt:0.0.0.0:45000~shs:" + id.publicKeyAsBase64String().substring(12))) + } +}