From b9635fa791b85a8a1d10d81b80060d4738a6067e Mon Sep 17 00:00:00 2001
From: Jingyu
Date: Tue, 24 Feb 2026 22:31:30 +0800
Subject: [PATCH 1/2] Improve CompactToken auth and generator; refine the
federation context interface
---
.../crypto/CachedCryptoIdentity.java | 18 +--
.../bosonnetwork/crypto/CryptoIdentity.java | 12 +-
.../io/bosonnetwork/service/Federation.java | 54 +++++++-
.../io/bosonnetwork/vertx/BosonVerticle.java | 16 +--
.../bosonnetwork/vertx/BufferInputStream.java | 96 +++++++++++++
.../vertx/BufferOutputStream.java | 57 ++++++++
.../java/io/bosonnetwork/web/AccessToken.java | 89 +++++++++---
.../bosonnetwork/web/CompactWebTokenAuth.java | 64 +++++----
.../web/CompactWebTokenAuthHandler.java | 2 +-
.../io/bosonnetwork/web/AccessTokenTest.java | 131 +++++++++++++++++-
.../web/CompactWebTokenAuthTest.java | 97 +++++++++++--
11 files changed, 538 insertions(+), 98 deletions(-)
create mode 100644 api/src/main/java/io/bosonnetwork/vertx/BufferInputStream.java
create mode 100644 api/src/main/java/io/bosonnetwork/vertx/BufferOutputStream.java
diff --git a/api/src/main/java/io/bosonnetwork/crypto/CachedCryptoIdentity.java b/api/src/main/java/io/bosonnetwork/crypto/CachedCryptoIdentity.java
index 5961799..209b9c4 100644
--- a/api/src/main/java/io/bosonnetwork/crypto/CachedCryptoIdentity.java
+++ b/api/src/main/java/io/bosonnetwork/crypto/CachedCryptoIdentity.java
@@ -77,12 +77,12 @@ public void initCache(Caffeine caffeine) {
/**
* Clears the cached {@link CryptoContext} instances.
- *
+ *
* This method invalidates all entries in the cache, ensuring that any
* cached cryptographic contexts are removed. Subsequent operations
* will no longer utilize the invalidated contexts and may trigger
* re-creation of new contexts as needed.
- *
+ *
* If the cache is uninitialized or null, this method has no effect.
*/
public void clearCache() {
@@ -95,22 +95,22 @@ private CryptoContext getContext(Id id) throws CryptoException {
}
/**
- * Performs one-shot encryption of the given data for the specified recipient.
+ * Performs one-shot encryption of the given data for the specified receiver.
*
- * This operation leverages a cached {@link CryptoContext} instance associated with the recipient,
+ * This operation leverages a cached {@link CryptoContext} instance associated with the receiver,
* reducing the overhead of repeatedly computing cryptographic contexts.
*
- * @param recipient the recipient's {@link Id}; must not be {@code null}
+ * @param receiver the receiver's {@link Id}; must not be {@code null}
* @param data the plaintext data to encrypt; must not be {@code null}
* @return the encrypted data including the nonce prepended
- * @throws NullPointerException if {@code recipient} or {@code data} is {@code null}
+ * @throws NullPointerException if {@code receiver} or {@code data} is {@code null}
* @throws CryptoException if an error occurs during encryption
*/
@Override
- public byte[] encrypt(Id recipient, byte[] data) throws CryptoException {
- Objects.requireNonNull(recipient, "recipient");
+ public byte[] encrypt(Id receiver, byte[] data) throws CryptoException {
+ Objects.requireNonNull(receiver, "receiver");
Objects.requireNonNull(data, "data");
- return getContext(recipient).encrypt(data);
+ return getContext(receiver).encrypt(data);
}
/**
diff --git a/api/src/main/java/io/bosonnetwork/crypto/CryptoIdentity.java b/api/src/main/java/io/bosonnetwork/crypto/CryptoIdentity.java
index e65ece3..62a6377 100644
--- a/api/src/main/java/io/bosonnetwork/crypto/CryptoIdentity.java
+++ b/api/src/main/java/io/bosonnetwork/crypto/CryptoIdentity.java
@@ -85,23 +85,23 @@ public boolean verify(byte[] data, byte[] signature) {
}
/**
- * Performs one-shot encryption of the given data for the specified recipient.
+ * Performs one-shot encryption of the given data for the specified receiver.
*
- * @param recipient the recipient's {@link Id}; must not be {@code null}
+ * @param receiver the receiver's {@link Id}; must not be {@code null}
* @param data the plaintext data to encrypt; must not be {@code null}
* @return the encrypted data including the nonce prepended
- * @throws NullPointerException if {@code recipient} or {@code data} is {@code null}
+ * @throws NullPointerException if {@code receiver} or {@code data} is {@code null}
* @throws CryptoException if an error occurs during encryption
*/
@Override
- public byte[] encrypt(Id recipient, byte[] data) throws CryptoException {
- Objects.requireNonNull(recipient, "recipient");
+ public byte[] encrypt(Id receiver, byte[] data) throws CryptoException {
+ Objects.requireNonNull(receiver, "receiver");
Objects.requireNonNull(data, "data");
try {
// TODO: how to avoid the memory copy?!
CryptoBox.Nonce nonce = CryptoBox.Nonce.random();
- CryptoBox.PublicKey pk = recipient.toEncryptionKey();
+ CryptoBox.PublicKey pk = receiver.toEncryptionKey();
CryptoBox.PrivateKey sk = encryptionKeyPair.privateKey();
byte[] cipher = CryptoBox.encrypt(data, pk, sk, nonce);
diff --git a/api/src/main/java/io/bosonnetwork/service/Federation.java b/api/src/main/java/io/bosonnetwork/service/Federation.java
index 67a7f94..73cf51a 100644
--- a/api/src/main/java/io/bosonnetwork/service/Federation.java
+++ b/api/src/main/java/io/bosonnetwork/service/Federation.java
@@ -26,6 +26,7 @@
import java.util.concurrent.CompletableFuture;
import io.bosonnetwork.Id;
+import io.bosonnetwork.web.CompactWebTokenAuth;
/**
* Federation manager (read-only) interface for the Boson Super Node services.
@@ -35,6 +36,22 @@
* specific services hosted by federated nodes.
*/
public interface Federation {
+ /**
+ * Represents the type of incident that can occur within the federation.
+ * This enumeration is used to classify and report issues related to
+ * federated nodes or services.
+ */
+ enum IncidentType {
+ /** Indicates a complete failure or unavailability of a service. */
+ SERVICE_OUTAGE,
+ /** Indicates a service encountering an operation failure or error. */
+ SERVICE_ERROR,
+ /** Indicates a poorly formed or invalid request received. */
+ MALFORMED_REQUEST,
+ /** Indicates an invalid or improperly constructed response sent. */
+ MALFORMED_RESPONSE
+ }
+
/**
* Retrieves a federated node by its ID.
*
@@ -43,7 +60,7 @@ public interface Federation {
* @return a {@link CompletableFuture} that completes with the {@link FederatedNode} object if found,
* or completes exceptionally/with null if the node cannot be found or federated
*/
- public CompletableFuture extends FederatedNode> getNode(Id nodeId, boolean federateIfNotExists);
+ CompletableFuture extends FederatedNode> getNode(Id nodeId, boolean federateIfNotExists);
/**
* Retrieves a federated node by its ID.
@@ -65,7 +82,7 @@ default CompletableFuture extends FederatedNode> getNode(Id nodeId) {
* @return a {@link CompletableFuture} that completes with {@code true} if the node exists,
* or {@code false} otherwise
*/
- public CompletableFuture existsNode(Id nodeId);
+ CompletableFuture existsNode(Id nodeId);
/**
* Retrieves information about a specific service hosted by a federated node.
@@ -75,7 +92,7 @@ default CompletableFuture extends FederatedNode> getNode(Id nodeId) {
* @return a {@link CompletableFuture} that completes with the list of {@link ServiceInfo} if found,
* or completes exceptionally/with null if the service cannot be located
*/
- public CompletableFuture> getServices(Id peerId, Id nodeId);
+ CompletableFuture> getServices(Id peerId, Id nodeId);
/**
* Retrieves a list of services associated with a specific peer identified by its ID.
@@ -85,5 +102,34 @@ default CompletableFuture extends FederatedNode> getNode(Id nodeId) {
* representing the services associated with the specified peer, or completes exceptionally
* if an error occurs while retrieving the services
*/
- public CompletableFuture> getServices(Id peerId);
+ CompletableFuture> getServices(Id peerId);
+
+ /**
+ * Reports an incident associated with a specific federated node and peer.
+ *
+ * @param nodeId the unique identifier of the federated node where the incident occurred
+ * @param peerId the unique identifier of the peer involved in the incident
+ * @param incident the type of incident being reported
+ * @param details a detailed description of the incident
+ * @return a {@link CompletableFuture} that completes when the incident has been reported successfully,
+ * or completes exceptionally if an error occurs during the reporting process
+ */
+ CompletableFuture reportIncident(Id nodeId, Id peerId, IncidentType incident, String details);
+
+ /**
+ * Retrieves the instance of {@link FederationAuthenticator} associated with this federation.
+ *
+ * @return the {@link FederationAuthenticator} responsible for managing authentication
+ * within the federation context.
+ */
+ FederationAuthenticator getAuthenticator();
+
+ /**
+ * Retrieves the instance of {@link CompactWebTokenAuth} used for handling
+ * web token authentication within the federation.
+ *
+ * @return the {@link CompactWebTokenAuth} instance responsible for managing
+ * web token authentication.
+ */
+ CompactWebTokenAuth getWebTokenAuthenticator();
}
\ No newline at end of file
diff --git a/api/src/main/java/io/bosonnetwork/vertx/BosonVerticle.java b/api/src/main/java/io/bosonnetwork/vertx/BosonVerticle.java
index 572f205..8569113 100644
--- a/api/src/main/java/io/bosonnetwork/vertx/BosonVerticle.java
+++ b/api/src/main/java/io/bosonnetwork/vertx/BosonVerticle.java
@@ -82,7 +82,7 @@ public final Vertx getVertx() {
*
* @return the Vert.x context
*/
- public final Context vertxContext() {
+ protected final Context vertxContext() {
return vertxContext;
}
@@ -103,7 +103,7 @@ public final String deploymentID() {
*
* @return the configuration as a {@link JsonObject}
*/
- public final JsonObject vertxConfig() {
+ protected final JsonObject vertxConfig() {
return vertxContext.config();
}
@@ -131,7 +131,7 @@ public final void init(Vertx vertx, Context context) {
* @param vertx the Vert.x instance
* @param context the Vert.x context
*/
- public void prepare(Vertx vertx, Context context) {
+ protected void prepare(Vertx vertx, Context context) {
this.vertx = vertx;
this.vertxContext = context;
}
@@ -172,7 +172,7 @@ public final void stop(Promise stopPromise) throws Exception {
*
* @return a future that completes when setup is finished
*/
- public abstract Future deploy();
+ protected abstract Future deploy();
/**
* Called during shutdown to perform asynchronous cleanup logic.
@@ -182,7 +182,7 @@ public final void stop(Promise stopPromise) throws Exception {
*
* @return a future that completes when teardown is finished
*/
- public abstract Future undeploy();
+ protected abstract Future undeploy();
/**
* Internal helper method to simulate deployment under Vert.x 5.x’s {@code Deployable} interface.
@@ -236,7 +236,7 @@ public final Future> undeploy(Context context) throws Exception {
*
* @param action the handler to run
*/
- public void runOnContext(Handler action) {
+ protected void runOnContext(Handler action) {
vertxContext.runOnContext(action);
}
@@ -249,7 +249,7 @@ public void runOnContext(Handler action) {
* @param the result type
* @return a future representing the blocking operation result
*/
- public Future executeBlocking(Callable blockingCodeHandler) {
+ protected Future executeBlocking(Callable blockingCodeHandler) {
return vertxContext.executeBlocking(blockingCodeHandler);
}
@@ -262,7 +262,7 @@ public Future executeBlocking(Callable blockingCodeHandler) {
* @param the result type
* @return a future representing the blocking operation result
*/
- public Future executeBlocking(Callable blockingCodeHandler, boolean ordered) {
+ protected Future executeBlocking(Callable blockingCodeHandler, boolean ordered) {
return vertxContext.executeBlocking(blockingCodeHandler, ordered);
}
}
\ No newline at end of file
diff --git a/api/src/main/java/io/bosonnetwork/vertx/BufferInputStream.java b/api/src/main/java/io/bosonnetwork/vertx/BufferInputStream.java
new file mode 100644
index 0000000..fdffa32
--- /dev/null
+++ b/api/src/main/java/io/bosonnetwork/vertx/BufferInputStream.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2023 - bosonnetwork.io
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.bosonnetwork.vertx;
+
+import java.io.InputStream;
+
+import io.vertx.core.buffer.Buffer;
+
+/**
+ * A wrapper for Vert.x Buffer that implements InputStream
+ * to allow zero-copy reading by Jackson and other utilities.
+ */
+public class BufferInputStream extends InputStream {
+ private final Buffer buffer;
+ private int pos;
+ private final int limit;
+ private int markPos = 0;
+
+ /**
+ * Constructs a new BufferInputStream instance to facilitate reading data from the specified
+ * Vert.x Buffer. The stream provides a zero-copy mechanism for efficient data handling.
+ *
+ * @param buffer the Vert.x Buffer from which data will be read
+ */
+ public BufferInputStream(Buffer buffer) {
+ this.buffer = buffer;
+ this.pos = 0;
+ this.limit = buffer.length();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int read() {
+ if (pos >= limit)
+ return -1;
+
+ return buffer.getByte(pos++) & 0xFF;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int read(byte[] b, int off, int len) {
+ if (pos >= limit)
+ return -1;
+
+ int available = limit - pos;
+ int toRead = Math.min(len, available);
+ buffer.getBytes(pos, pos + toRead, b, off);
+ pos += toRead;
+ return toRead;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int available() {
+ return limit - pos;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean markSupported() {
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public synchronized void mark(int readlimit) {
+ markPos = pos;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public synchronized void reset() {
+ pos = markPos;
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/io/bosonnetwork/vertx/BufferOutputStream.java b/api/src/main/java/io/bosonnetwork/vertx/BufferOutputStream.java
new file mode 100644
index 0000000..4213856
--- /dev/null
+++ b/api/src/main/java/io/bosonnetwork/vertx/BufferOutputStream.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2023 - bosonnetwork.io
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.bosonnetwork.vertx;
+
+import java.io.OutputStream;
+
+import io.vertx.core.buffer.Buffer;
+
+/**
+ * A wrapper for Vert.x Buffer that implements OutputStream
+ * to allow zero-copy writing by Jackson and other utilities.
+ */
+public class BufferOutputStream extends OutputStream {
+ private final Buffer buffer;
+
+ /**
+ * Constructs a new BufferOutputStream instance that wraps the provided Vert.x Buffer.
+ * This allows data to be written to the Buffer through a standard OutputStream interface.
+ *
+ * @param buffer the Vert.x Buffer to wrap and write data into
+ */
+ public BufferOutputStream(Buffer buffer) {
+ this.buffer = buffer;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void write(int b) {
+ buffer.appendByte((byte) b);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void write(byte[] b, int off, int len) {
+ buffer.appendBytes(b, off, len);
+ }
+}
\ No newline at end of file
diff --git a/api/src/main/java/io/bosonnetwork/web/AccessToken.java b/api/src/main/java/io/bosonnetwork/web/AccessToken.java
index 9d90f2d..69dd759 100644
--- a/api/src/main/java/io/bosonnetwork/web/AccessToken.java
+++ b/api/src/main/java/io/bosonnetwork/web/AccessToken.java
@@ -23,7 +23,6 @@
package io.bosonnetwork.web;
import java.io.IOException;
-import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
@@ -41,35 +40,58 @@
*
*/
public class AccessToken {
- private static final Base64.Encoder B64encoder = Base64.getUrlEncoder().withoutPadding();
+ private static final long MAX_TOKEN_LIFETIME = 30 * 60; // 30 minutes in seconds
private final Identity issuer;
+ private final long defaultTTL;
/**
* Creates a new AccessToken generator for the given identity.
- *
+ *
* @param issuer the identity that will issue and sign the tokens
+ * @param defaultTTL the default time-to-live in seconds. If 0, the default value is used
*/
- public AccessToken(Identity issuer) {
+ public AccessToken(Identity issuer, long defaultTTL) {
+ if (defaultTTL < 0)
+ throw new IllegalArgumentException("defaultTTL must be positive");
+
this.issuer = Objects.requireNonNull(issuer, "issuer cannot be null");
+ this.defaultTTL = defaultTTL == 0 ? MAX_TOKEN_LIFETIME : defaultTTL;
+ }
+
+ /**
+ * Creates a new AccessToken generator for the given identity with the default TTL.
+ *
+ * @param issuer the identity that will issue and sign the tokens
+ */
+ public AccessToken(Identity issuer) {
+ this(issuer, 0);
}
/**
* Generates a signed token.
- *
- * @param subject the subject ID (usually the same as issuer or a user ID if issuer is a device)
- * @param associated the associated entity ID (optional, e.g. device ID)
- * @param audience the audience ID (the server node ID)
- * @param scope the scope string (optional)
- * @param ttl the time-to-live in seconds
+ *
+ * @param subject the subject ID (usually the same as issuer or a user ID if issuer is a device)
+ * @param associated the associated entity ID (optional, e.g.; device ID)
+ * @param audience the audience ID (the server node ID)
+ * @param scope the scope string (optional)
+ * @param ttl the time-to-live in seconds
* @return the generated token string
*/
- private String generate(Id subject, Id associated, Id audience, String scope, long ttl) {
+ protected String generate(Id subject, Id associated, Id audience, String scope, long ttl) {
Objects.requireNonNull(subject, "subject cannot be null");
- Objects.requireNonNull(audience, "audience cannot be null");
+ Objects.requireNonNull(audience, "audience cannot be null");
if (ttl <= 0)
throw new IllegalArgumentException("ttl must be positive");
+ if (associated != null) {
+ if (!associated.equals(issuer.getId()))
+ throw new IllegalArgumentException("associated must be the issuer ID");
+ } else {
+ if (!subject.equals(issuer.getId()))
+ throw new IllegalArgumentException("subject must be the issuer ID");
+ }
+
Map claims = new LinkedHashMap<>();
claims.put("jti", Random.randomBytes(24));
claims.put("iss", issuer.getId().bytes());
@@ -77,9 +99,9 @@ private String generate(Id subject, Id associated, Id audience, String scope, lo
if (associated != null)
claims.put("asc", associated.bytes());
claims.put("aud", audience.bytes());
- if (scope != null && !scope.isEmpty())
+ if (scope != null && !scope.isEmpty())
claims.put("scp", scope);
-
+
long now = System.currentTimeMillis() / 1000;
claims.put("exp", now + ttl);
@@ -92,31 +114,54 @@ private String generate(Id subject, Id associated, Id audience, String scope, lo
byte[] sig = issuer.sign(payload);
- return B64encoder.encodeToString(payload) + "." + B64encoder.encodeToString(sig);
+ return Json.BASE64_ENCODER.encodeToString(payload) + "." + Json.BASE64_ENCODER.encodeToString(sig);
}
/**
* Generates a signed token as the subject and without associated entity.
*
* @param audience the audience ID (the server node ID)
- * @param scope the scope string (optional)
- * @param ttl the time-to-live in seconds
+ * @param scope the scope string (optional)
+ * @param ttl the time-to-live in seconds
* @return the generated token string
*/
public String generate(Id audience, String scope, long ttl) {
- return generate(issuer.getId(), null, audience, scope, ttl);
+ return generate(issuer.getId(), null, audience, scope, ttl == 0 ? defaultTTL : ttl);
+ }
+
+ /**
+ * Generates a signed token as the subject and without associated entity.
+ *
+ * @param audience the audience ID (the server node ID)
+ * @param scope the scope string (optional)
+ * @return the generated token string
+ */
+ public String generate(Id audience, String scope) {
+ return generate(audience, scope, 0);
}
/**
* Generates a signed token as the associated entity.
*
- * @param subject the subject ID (usually the same as issuer or a user ID if issuer is a device)
+ * @param subject the subject ID (usually the same as issuer or a user ID if issuer is a device)
* @param audience the audience ID (the server node ID)
- * @param scope the scope string (optional)
- * @param ttl the time-to-live in seconds
+ * @param scope the scope string (optional)
+ * @param ttl the time-to-live in seconds
* @return the generated token string
*/
public String generate(Id subject, Id audience, String scope, long ttl) {
- return generate(subject, issuer.getId(), audience, scope, ttl);
+ return generate(subject, issuer.getId(), audience, scope, ttl == 0 ? defaultTTL : ttl);
+ }
+
+ /**
+ * Generates a signed token as the associated entity.
+ *
+ * @param subject the subject ID (usually the same as issuer or a user ID if issuer is a device)
+ * @param audience the audience ID (the server node ID)
+ * @param scope the scope string (optional)
+ * @return the generated token string
+ */
+ public String generate(Id subject, Id audience, String scope) {
+ return generate(subject, audience, scope, 0);
}
}
\ No newline at end of file
diff --git a/api/src/main/java/io/bosonnetwork/web/CompactWebTokenAuth.java b/api/src/main/java/io/bosonnetwork/web/CompactWebTokenAuth.java
index 3893dce..dd0b489 100644
--- a/api/src/main/java/io/bosonnetwork/web/CompactWebTokenAuth.java
+++ b/api/src/main/java/io/bosonnetwork/web/CompactWebTokenAuth.java
@@ -23,7 +23,6 @@
package io.bosonnetwork.web;
import java.io.IOException;
-import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
@@ -39,12 +38,12 @@
import io.bosonnetwork.Id;
import io.bosonnetwork.Identity;
import io.bosonnetwork.crypto.Random;
+import io.bosonnetwork.json.Json;
import io.bosonnetwork.service.ClientAuthenticator;
import io.bosonnetwork.service.ClientDevice;
import io.bosonnetwork.service.ClientUser;
import io.bosonnetwork.service.FederatedNode;
import io.bosonnetwork.service.ServiceInfo;
-import io.bosonnetwork.json.Json;
import io.bosonnetwork.utils.Pair;
/**
@@ -82,11 +81,6 @@
*
*/
public class CompactWebTokenAuth implements AuthenticationProvider {
- /** Base64 URL encoder without padding. */
- protected static final Base64.Encoder B64encoder = Base64.getUrlEncoder().withoutPadding();
- /** Base64 URL decoder. */
- protected static final Base64.Decoder B64decoder = Base64.getUrlDecoder();
-
private static final long MAX_SERVER_ISSUED_TOKEN_LIFETIME = 14 * 24 * 60 * 60; // 14 days in seconds
private static final long MAX_CLIENT_ISSUED_TOKEN_LIFETIME = 30 * 60; // 30 minutes in seconds
private static final int DEFAULT_LEEWAY = 5 * 60; // 5 minutes in seconds
@@ -122,7 +116,7 @@ public interface UserRepository {
* by leveraging the given authenticator for security purposes.
*
* @param authenticator the {@code ClientAuthenticator} instance used to validate client authentication
- * @return a {@code UserRepository} implementation that utilizes the given {@code ClientAuthenticator}
+ * @return a {@code UserRepository} implementation that uses the given {@code ClientAuthenticator}
*/
static UserRepository fromClientAuthenticator(ClientAuthenticator authenticator) {
return new AuthenticatorUserRepo(authenticator);
@@ -272,7 +266,7 @@ private CompactWebTokenAuth(Identity identity, UserRepository userRepository,
* Creates a new instance of CompactWebTokenAuth.
*
* @param identity the identity of the current server node (used for signing and verification)
- * @param userRepository the repository to lookup token subjects and associated entities
+ * @param userRepository the repository to look up token subjects and associated entities
* @param maxServerIssuedTokenLifetime maximum lifetime for tokens issued by this server (seconds)
* @param maxClientIssuedTokenLifetime maximum lifetime for tokens issued by clients (seconds)
* @param leeway allowed clock skew (seconds)
@@ -333,8 +327,8 @@ public Future authenticate(Credentials credentials) {
final JsonObject claims;
try {
- payload = B64decoder.decode(token.substring(0, index));
- sig = B64decoder.decode(token.substring(index + 1));
+ payload = Json.BASE64_DECODER.decode(token.substring(0, index));
+ sig = Json.BASE64_DECODER.decode(token.substring(index + 1));
claims = new JsonObject(Json.cborMapper().readValue(payload, Json.mapType()));
} catch (IllegalArgumentException | IOException e) {
return Future.failedFuture("Invalid authorization token: format error");
@@ -444,7 +438,7 @@ public Future authenticate(Credentials credentials) {
return Future.failedFuture("Invalid authorization token: wrong issuer");
}
*/
- if (!issuer.equals(Objects.requireNonNullElse(associated, subject)))
+ if (!Objects.equals(issuer, subject) && !Objects.equals(issuer, associated))
return Future.failedFuture("Invalid authorization token: wrong issuer");
if (audience == null)
@@ -463,9 +457,9 @@ public Future authenticate(Credentials credentials) {
return Future.failedFuture("Invalid authorization token: life time too long");
}
- // verify the signature
- if (!issuer.toSignatureKey().verify(payload, sig))
+ if (!issuer.toSignatureKey().verify(payload, sig)) {
return Future.failedFuture("Invalid authorization token: signature verification failed");
+ }
final String scope = claims.containsKey("scp") ? claims.getString("scp") : null;
@@ -536,6 +530,18 @@ public Future authenticate(Credentials credentials) {
});
}
+ private String encodeToken(Map claims) {
+ final byte[] payload;
+ try {
+ payload = Json.cborMapper().writeValueAsBytes(claims);
+ } catch (IOException e) {
+ throw new RuntimeException("INTERNAL ERROR: JSON serialization");
+ }
+
+ final byte[] sig = identity.sign(payload);
+ return Json.BASE64_ENCODER.encodeToString(payload) + "." + Json.BASE64_ENCODER.encodeToString(sig);
+ }
+
/**
* Generates a new token with specific claims.
*
@@ -544,30 +550,29 @@ public Future authenticate(Credentials credentials) {
* @throws IllegalArgumentException if expiration is invalid
*/
public String generateToken(Map claims) {
- Map _claims;
+ Map _claims = null;
long now = System.currentTimeMillis() / 1000;
if (claims.containsKey("exp") && claims.get("exp") != null) {
long expiration = (Long) claims.get("exp");
if (expiration <= 0 || expiration > now + maxServerIssuedTokenLifetime)
throw new IllegalArgumentException("Invalid expiration");
-
- _claims = claims;
} else {
_claims = new LinkedHashMap<>(claims);
_claims.put("exp", now + maxServerIssuedTokenLifetime);
}
- final byte[] payload;
- try {
- payload = Json.cborMapper().writeValueAsBytes(_claims);
- } catch (IOException e) {
- throw new RuntimeException("INTERNAL ERROR: JSON serialization");
+ if (!claims.containsKey("jti")) {
+ if (_claims == null)
+ _claims = new LinkedHashMap<>(claims);
+
+ _claims.put("jti", Random.randomBytes(24));
}
- final byte[] sig = identity.sign(payload);
+ if (_claims == null)
+ _claims = claims;
- return B64encoder.encodeToString(payload) + "." + B64encoder.encodeToString(sig);
+ return encodeToken(_claims);
}
/**
@@ -576,17 +581,16 @@ public String generateToken(Map claims) {
* @param subject the subject ID
* @param associated the associated entity ID (optional, can be null)
* @param scope the scope string (optional, can be null)
- * @param expiration the expiration time in seconds (0 for default server lifetime)
+ * @param ttl the time-to-live in seconds (0 for default server lifetime)
* @return the generated token string
* @throws IllegalArgumentException if expiration is invalid
*/
- public String generateToken(Id subject, Id associated, String scope, long expiration) {
+ public String generateToken(Id subject, Id associated, String scope, long ttl) {
Objects.requireNonNull(subject);
- if (expiration < 0 || expiration > maxServerIssuedTokenLifetime)
+ if (ttl < 0 || ttl > maxServerIssuedTokenLifetime)
throw new IllegalArgumentException("Invalid expiration");
- if (expiration == 0)
- expiration = System.currentTimeMillis() / 1000 + maxServerIssuedTokenLifetime;
+ long expiration = System.currentTimeMillis() / 1000 + (ttl == 0 ? maxServerIssuedTokenLifetime : ttl);
Map claims = new LinkedHashMap<>(5);
claims.put("jti", Random.randomBytes(24));
@@ -596,7 +600,7 @@ public String generateToken(Id subject, Id associated, String scope, long expira
if (scope != null && !scope.isEmpty())
claims.put("scp", scope);
claims.put("exp", expiration);
- return generateToken(claims);
+ return encodeToken(claims);
}
/**
diff --git a/api/src/main/java/io/bosonnetwork/web/CompactWebTokenAuthHandler.java b/api/src/main/java/io/bosonnetwork/web/CompactWebTokenAuthHandler.java
index a43e352..c231f7e 100644
--- a/api/src/main/java/io/bosonnetwork/web/CompactWebTokenAuthHandler.java
+++ b/api/src/main/java/io/bosonnetwork/web/CompactWebTokenAuthHandler.java
@@ -106,7 +106,7 @@ public Future authenticate(RoutingContext context) {
return authProvider.authenticate(credentials)
.andThen(op -> audit.audit(Marker.AUTHENTICATION, op.succeeded()))
- .recover(err -> Future.failedFuture(new HttpException(401, err)));
+ .recover(err -> Future.failedFuture(new HttpException(401, err.getMessage())));
});
}
diff --git a/api/src/test/java/io/bosonnetwork/web/AccessTokenTest.java b/api/src/test/java/io/bosonnetwork/web/AccessTokenTest.java
index 9ca7c55..37462ae 100644
--- a/api/src/test/java/io/bosonnetwork/web/AccessTokenTest.java
+++ b/api/src/test/java/io/bosonnetwork/web/AccessTokenTest.java
@@ -2,8 +2,12 @@
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.Assertions.assertTrue;
import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.core.Vertx;
import io.vertx.ext.auth.authentication.TokenCredentials;
import org.junit.jupiter.api.Assertions;
@@ -15,10 +19,12 @@
import io.bosonnetwork.Id;
import io.bosonnetwork.Identity;
import io.bosonnetwork.crypto.CryptoIdentity;
+import io.bosonnetwork.json.Json;
import io.bosonnetwork.service.ClientDevice;
import io.bosonnetwork.service.ClientUser;
@ExtendWith(VertxExtension.class)
+@SuppressWarnings("CodeBlock2Expr")
public class AccessTokenTest {
private static final Identity superNodeIdentity = new CryptoIdentity();
private static final Identity aliceIdentity = new CryptoIdentity();
@@ -43,13 +49,21 @@ public Future> getAssociated(Id subject, Id associated) {
private static final CompactWebTokenAuth auth = CompactWebTokenAuth.create(superNodeIdentity, repo, 3600, 3600, 0);
+ private static void printToken(String token) {
+ System.out.println("Token: " + token);
+ String[] parts = token.split("\\.");
+ String payload = Json.toString(Json.parse(Json.BASE64_DECODER.decode(parts[0])));
+ System.out.println(" - " + payload);
+ System.out.println(" - " + parts[1]);
+ }
+
@Test
void testGenerateUserToken(VertxTestContext context) {
AccessToken accessToken = new AccessToken(aliceIdentity);
Id audience = superNodeIdentity.getId();
- String token = accessToken.generate(audience, "read write", 60);
- System.out.println("Generated Token: " + token);
+ String token = accessToken.generate(audience, "read write");
+ printToken(token);
// Verify using CompactWebTokenAuth
auth.authenticate(new TokenCredentials(token)).onComplete(context.succeeding(user -> {
@@ -69,7 +83,8 @@ void testGenerateDeviceToken(VertxTestContext context) {
AccessToken accessToken = new AccessToken(iPadIdentity);
Id audience = superNodeIdentity.getId();
- String token = accessToken.generate(alice.getId(), audience, "read write", 60);
+ String token = accessToken.generate(alice.getId(), audience, "read write");
+ printToken(token);
auth.authenticate(new TokenCredentials(token)).onComplete(context.succeeding(user -> {
context.verify(() -> {
@@ -82,4 +97,114 @@ void testGenerateDeviceToken(VertxTestContext context) {
});
}));
}
+
+ @Test
+ void testGenerateUserTokenWithWrongAudience(VertxTestContext context) {
+ AccessToken accessToken = new AccessToken(aliceIdentity);
+ Id audience = Id.random();
+
+ String token = accessToken.generate(audience, "read write");
+ printToken(token);
+
+ // Verify using CompactWebTokenAuth
+ auth.authenticate(new TokenCredentials(token)).onComplete(ar -> {
+ context.verify(() -> {
+ assertTrue(ar.failed());
+ assertTrue(ar.cause().getMessage().contains("wrong audience"));
+ context.completeNow();
+ });
+ });
+ }
+
+ @Test
+ void testGenerateDeviceTokenWithWrongAudience(VertxTestContext context) {
+ AccessToken accessToken = new AccessToken(iPadIdentity);
+ Id audience = Id.random();
+
+ String token = accessToken.generate(alice.getId(), audience, "read write");
+ printToken(token);
+
+ auth.authenticate(new TokenCredentials(token)).onComplete(ar -> {
+ context.verify(() -> {
+ assertTrue(ar.failed());
+ assertTrue(ar.cause().getMessage().contains("wrong audience"));
+ context.completeNow();
+ });
+ });
+ }
+
+ @Test
+ void testExpiredUserAndDeviceToken(Vertx vertx, VertxTestContext context) {
+ Id audience = superNodeIdentity.getId();
+
+ String userToken = new AccessToken(aliceIdentity, 10).generate(audience, "read write");
+ printToken(userToken);
+
+ String deviceToken = new AccessToken(iPadIdentity, 10).generate(alice.getId(), audience, "read write");
+ printToken(deviceToken);
+
+ Promise promise = Promise.promise();
+ vertx.setTimer(10000, l -> promise.complete());
+ System.out.println("Waiting for token expiration...");
+
+ promise.future().compose(v -> {
+ Future f1 = auth.authenticate(new TokenCredentials(userToken)).onComplete(ar -> {
+ context.verify(() -> {
+ assertTrue(ar.failed());
+ assertTrue(ar.cause().getMessage().contains("expired"));
+ });
+ }).mapEmpty();
+
+ Future f2 = auth.authenticate(new TokenCredentials(deviceToken)).onComplete(ar -> {
+ context.verify(() -> {
+ assertTrue(ar.failed());
+ assertTrue(ar.cause().getMessage().contains("expired"));
+ });
+ }).mapEmpty();
+
+ return Future.join(f1, f2).otherwiseEmpty();
+ }).onComplete(context.succeedingThenComplete());
+ }
+
+ @Test
+ void testGenerateUserTokenWithWrongSubject() {
+ AccessToken accessToken = new AccessToken(aliceIdentity);
+ Id audience = superNodeIdentity.getId();
+
+ Exception e = assertThrows(IllegalArgumentException.class, () -> {
+ accessToken.generate(Id.random(), null, audience, "read write", 60);
+ });
+ assertTrue(e.getMessage().contains("subject must be the issuer ID"));
+ }
+
+ @Test
+ void testGenerateDeviceTokenWithWrongSubject() {
+ AccessToken accessToken = new AccessToken(iPadIdentity);
+ Id audience = superNodeIdentity.getId();
+
+ Exception e = assertThrows(IllegalArgumentException.class, () -> {
+ accessToken.generate(alice.getId(), Id.random(), audience, "read write", 60);
+ });
+ assertTrue(e.getMessage().contains("associated must be the issuer ID"));
+ }
+
+ @Test
+ void testGenerateUserTokenWithoutAudience() {
+ AccessToken accessToken = new AccessToken(aliceIdentity);
+
+ Exception e = assertThrows(NullPointerException.class, () -> {
+ accessToken.generate(null, "read write");
+ });
+ assertTrue(e.getMessage().contains("audience cannot be null"));
+ }
+
+ @Test
+ void testGenerateDeviceTokenWithoutAudience() {
+ AccessToken accessToken = new AccessToken(iPadIdentity);
+
+ Exception e = assertThrows(NullPointerException.class, () -> {
+ accessToken.generate(alice.getId(), iPad.getId(), null, "read write", 60);
+ });
+ assertTrue(e.getMessage().contains("audience cannot be null"));
+ }
}
\ No newline at end of file
diff --git a/api/src/test/java/io/bosonnetwork/web/CompactWebTokenAuthTest.java b/api/src/test/java/io/bosonnetwork/web/CompactWebTokenAuthTest.java
index dbf5c30..0bef113 100644
--- a/api/src/test/java/io/bosonnetwork/web/CompactWebTokenAuthTest.java
+++ b/api/src/test/java/io/bosonnetwork/web/CompactWebTokenAuthTest.java
@@ -31,8 +31,9 @@
import io.bosonnetwork.json.Json;
@ExtendWith(VertxExtension.class)
+@SuppressWarnings("CodeBlock2Expr")
public class CompactWebTokenAuthTest {
- private static final long DEFAULT_LIFETIME = 10;
+ private static final long DEFAULT_LIFETIME = 10; // 10 seconds
private static final Identity superNodeIdentity = new CryptoIdentity();
private static final Signature.KeyPair aliceKeyPair = Signature.KeyPair.random();
private static final Signature.KeyPair iPadKeyPair = Signature.KeyPair.random();
@@ -58,10 +59,18 @@ public Future> getAssociated(Id subject, Id associated) {
private static final CompactWebTokenAuth auth = CompactWebTokenAuth.create(superNodeIdentity, repo,
DEFAULT_LIFETIME, DEFAULT_LIFETIME, 0);
+ private static void printToken(String token) {
+ System.out.println("Token: " + token);
+ String[] parts = token.split("\\.");
+ String payload = Json.toString(Json.parse(Json.BASE64_DECODER.decode(parts[0])));
+ System.out.println(" - " + payload);
+ System.out.println(" - " + parts[1]);
+ }
+
@Test
void testSuperNodeIssuedToken(VertxTestContext context) {
String userToken = auth.generateToken(alice.getId(), "test");
- System.out.println(userToken);
+ printToken(userToken);
Future f1 = auth.authenticate(new TokenCredentials(userToken)).andThen(context.succeeding(user -> {
context.verify(() -> {
assertNotNull(user);
@@ -78,7 +87,7 @@ void testSuperNodeIssuedToken(VertxTestContext context) {
}));
String deviceToken = auth.generateToken(alice.getId(), iPad.getId(), "test");
- System.out.println(deviceToken);
+ printToken(deviceToken);
Future f2 = auth.authenticate(new TokenCredentials(deviceToken)).andThen(context.succeeding(user -> {
context.verify(() -> {
assertNotNull(user);
@@ -99,42 +108,42 @@ void testSuperNodeIssuedToken(VertxTestContext context) {
@Test
void testSuperNodeIssuedAndExpiredToken(Vertx vertx, VertxTestContext context) {
String userToken = auth.generateToken(alice.getId(), "test");
- System.out.println(userToken);
+ printToken(userToken);
String deviceToken = auth.generateToken(alice.getId(), iPad.getId(), "test");
- System.out.println(deviceToken);
+ printToken(deviceToken);
Promise promise = Promise.promise();
vertx.setTimer(10000, l -> promise.complete());
System.out.println("Waiting for token expiration...");
promise.future().compose(v -> {
- Future f1 = auth.authenticate(new TokenCredentials(userToken)).andThen(ar -> {
+ Future f1 = auth.authenticate(new TokenCredentials(userToken)).onComplete(ar -> {
context.verify(() -> {
assertTrue(ar.failed());
assertTrue(ar.cause().getMessage().contains("expired"));
});
- }).otherwiseEmpty();
+ });
- Future f2 = auth.authenticate(new TokenCredentials(deviceToken)).andThen(ar -> {
+ Future f2 = auth.authenticate(new TokenCredentials(deviceToken)).onComplete(ar -> {
context.verify(() -> {
assertTrue(ar.failed());
assertTrue(ar.cause().getMessage().contains("expired"));
});
- }).otherwiseEmpty();
+ });
- return Future.all(f1, f2);
+ return Future.all(f1, f2).otherwiseEmpty();
}).andThen(context.succeedingThenComplete());
}
@Test
- void testSuperNodeIssuedAndInvalidSigToken(Vertx vertx, VertxTestContext context) {
+ void testSuperNodeIssuedAndInvalidSigToken(VertxTestContext context) {
String userToken = auth.generateToken(alice.getId(), "test");
- System.out.println(userToken);
+ printToken(userToken);
byte[] sig = Json.BASE64_DECODER.decode(userToken.substring(userToken.lastIndexOf('.') + 1));
sig[0] = (byte) ~sig[0];
String invalidUserToken = userToken.substring(0, userToken.lastIndexOf('.')) + '.' + Json.BASE64_ENCODER.encodeToString(sig);
String deviceToken = auth.generateToken(alice.getId(), iPad.getId(), "test");
- System.out.println(deviceToken);
+ printToken(deviceToken);
sig = Json.BASE64_DECODER.decode(deviceToken.substring(deviceToken.lastIndexOf('.') + 1));
sig[0] = (byte) ~sig[0];
String invalidDeviceToken = deviceToken.substring(0, deviceToken.lastIndexOf('.')) + '.' + Json.BASE64_ENCODER.encodeToString(sig);
@@ -198,7 +207,7 @@ private String generateClientToken(Signature.KeyPair signer, Id subject, Id asso
@Test
void testClientIssuedToken(VertxTestContext context) throws Exception {
String userToken = generateClientToken(aliceKeyPair, alice.getId(), null, superNodeIdentity.getId(), 0, "test");
- System.out.println("ClientToken: " + userToken);
+ printToken(userToken);
auth.authenticate(new TokenCredentials(userToken)).onComplete(context.succeeding(user -> {
context.verify(() -> {
@@ -216,7 +225,7 @@ void testClientIssuedToken(VertxTestContext context) throws Exception {
@Test
void testDeviceIssuedToken(VertxTestContext context) throws Exception {
String deviceToken = generateClientToken(iPadKeyPair, alice.getId(), iPad.getId(), superNodeIdentity.getId(), 0, "test");
- System.out.println("DeviceToken: " + deviceToken);
+ printToken(deviceToken);
auth.authenticate(new TokenCredentials(deviceToken)).onComplete(context.succeeding(user -> {
context.verify(() -> {
@@ -242,5 +251,63 @@ void testClientIssuedTokenWrongAudience(VertxTestContext context) throws Excepti
context.completeNow();
});
});
+
+ // Wrong audience (random ID)
+ String deviceToken = generateClientToken(iPadKeyPair, alice.getId(), iPad.getId(), Id.random(), 0, "test");
+
+ auth.authenticate(new TokenCredentials(deviceToken)).onComplete(ar -> {
+ context.verify(() -> {
+ assertTrue(ar.failed());
+ assertTrue(ar.cause().getMessage().contains("wrong audience"));
+ context.completeNow();
+ });
+ });
+ }
+
+ @Test
+ void testClientIssuedTokenWithoutAudience(VertxTestContext context) throws Exception {
+ String userToken = generateClientToken(aliceKeyPair, alice.getId(), null, null, 0, "test");
+
+ auth.authenticate(new TokenCredentials(userToken)).onComplete(ar -> {
+ context.verify(() -> {
+ assertTrue(ar.failed());
+ assertTrue(ar.cause().getMessage().contains("missing audience"));
+ context.completeNow();
+ });
+ });
+
+ String deviceToken = generateClientToken(iPadKeyPair, alice.getId(), iPad.getId(), null, 0, "test");
+
+ auth.authenticate(new TokenCredentials(deviceToken)).onComplete(ar -> {
+ context.verify(() -> {
+ assertTrue(ar.failed());
+ assertTrue(ar.cause().getMessage().contains("missing audience"));
+ context.completeNow();
+ });
+ });
+ }
+
+ @Test
+ void testClientIssuedTokenWrongIssuer(VertxTestContext context) throws Exception {
+ // Wrong issuer
+ String userToken = generateClientToken(aliceKeyPair, Id.random(), null, Id.random(), 0, "test");
+
+ auth.authenticate(new TokenCredentials(userToken)).onComplete(ar -> {
+ context.verify(() -> {
+ assertTrue(ar.failed());
+ assertTrue(ar.cause().getMessage().contains("wrong issuer"));
+ context.completeNow();
+ });
+ });
+
+ String deviceToken = generateClientToken(iPadKeyPair, alice.getId(), Id.random(), Id.random(), 0, "test");
+
+ auth.authenticate(new TokenCredentials(deviceToken)).onComplete(ar -> {
+ context.verify(() -> {
+ assertTrue(ar.failed());
+ assertTrue(ar.cause().getMessage().contains("wrong issuer"));
+ context.completeNow();
+ });
+ });
}
}
\ No newline at end of file
From 63ff7a44d5ca5335c6c3e8df05fc1788c898d10c Mon Sep 17 00:00:00 2001
From: Jingyu
Date: Wed, 25 Feb 2026 11:58:27 +0800
Subject: [PATCH 2/2] clean up BosonVerticle interface for Vert.x 4.5.x and
5.0.x, reduce method name pollution & ambiguity
---
.../io/bosonnetwork/vertx/BosonVerticle.java | 240 +++++++++++++-----
.../io/bosonnetwork/kademlia/KadNode.java | 6 +-
.../io/bosonnetwork/kademlia/impl/DHT.java | 6 +-
.../kademlia/rpc/RPCServerTests.java | 6 +-
4 files changed, 186 insertions(+), 72 deletions(-)
diff --git a/api/src/main/java/io/bosonnetwork/vertx/BosonVerticle.java b/api/src/main/java/io/bosonnetwork/vertx/BosonVerticle.java
index 8569113..9aea2f0 100644
--- a/api/src/main/java/io/bosonnetwork/vertx/BosonVerticle.java
+++ b/api/src/main/java/io/bosonnetwork/vertx/BosonVerticle.java
@@ -57,7 +57,7 @@
* {@link #undeploy()}. This ensures forward compatibility with Vert.x 5’s {@code VerticleBase}.
*
*/
-public abstract class BosonVerticle implements /* Verticle, */ Deployable {
+public abstract class BosonVerticle implements Deployable {
/**
* Reference to the Vert.x instance that deployed this verticle
*/
@@ -108,23 +108,9 @@ protected final JsonObject vertxConfig() {
}
/**
- * Initializes the Verticle.
+ * Prepares this verticle for deployment.
*
- * This method is called by Vert.x when the Verticle instance is deployed.
- * User code should not call this directly.
- *
- *
- * @param vertx the Vert.x instance
- * @param context the context associated with this Verticle
- */
- public final void init(Vertx vertx, Context context) {
- prepare(vertx, context);
- }
-
- /**
- * Prepares this verticle for execution.
- *
- * This method is invoked internally by {@link #init(Vertx, Context)} and can be overridden
+ * This method is invoked internally by {@link #deploy(Context)} and can be overridden
* if additional setup is needed before deployment.
*
*
@@ -137,37 +123,9 @@ protected void prepare(Vertx vertx, Context context) {
}
/**
- * Called when the Verticle is started.
- *
- * This implementation delegates to {@link #deploy()}, which should return a {@link Future}
- * that completes when startup is done.
- *
- *
- * @param startPromise a promise that should be completed when startup is done
- * @throws Exception if startup fails
- */
- public final void start(Promise startPromise) throws Exception {
- deploy().onComplete(startPromise);
- }
-
- /**
- * Called when the Verticle is stopped.
- *
- * This implementation delegates to {@link #undeploy()}, which should return a {@link Future}
- * that completes when shutdown is done.
- *
- *
- * @param stopPromise a promise that should be completed when shutdown is done
- * @throws Exception if shutdown fails
- */
- public final void stop(Promise stopPromise) throws Exception {
- undeploy().onComplete(stopPromise);
- }
-
- /**
- * Called during startup to perform asynchronous initialization logic.
+ * Called during deployment to perform asynchronous initialization logic.
*
- * This method is invoked by {@link #start(Promise)}.
+ * This method is invoked by {@link #deploy(Context)}.
*
*
* @return a future that completes when setup is finished
@@ -175,9 +133,9 @@ public final void stop(Promise stopPromise) throws Exception {
protected abstract Future deploy();
/**
- * Called during shutdown to perform asynchronous cleanup logic.
+ * Called during undeployment to perform asynchronous cleanup logic.
*
- * This method is invoked by {@link #stop(Promise)}.
+ * This method is invoked by {@link #undeploy(Context)}.
*
*
* @return a future that completes when teardown is finished
@@ -185,14 +143,12 @@ public final void stop(Promise stopPromise) throws Exception {
protected abstract Future undeploy();
/**
- * Internal helper method to simulate deployment under Vert.x 5.x’s {@code Deployable} interface.
+ * Start the deployable.
*
- * This should not be called directly by user code. It is used by Vert.x internals or
- * integration layers that work with Vert.x 5.x’s deployment model.
- *
+ * Vert.x calls this method when deploying this deployable. You do not call it yourself.
*
- * @param context the Vert.x context
- * @return a future that completes when deployment is finished
+ * @param context the Vert.x context assigned to this deployable
+ * @return a future signaling the start-up completion
* @throws Exception if deployment fails
*/
public final Future> deploy(Context context) throws Exception {
@@ -209,14 +165,12 @@ public final Future> deploy(Context context) throws Exception {
}
/**
- * Internal helper method to simulate undeployment under Vert.x 5.x’s {@code Deployable} interface.
+ * Stop the deployable.
*
- * User code should not call this directly. It is used by Vert.x internals or
- * integration layers that work with Vert.x 5.x’s deployment model.
- *
+ * Vert.x calls this method when undeploying this deployable. You do not call it yourself.
*
- * @param context the Vert.x context
- * @return a future that completes when undeployment is finished
+ * @param context the Vert.x context assigned to this deployable
+ * @return a future signaling the clean-up completion
* @throws Exception if undeployment fails
*/
public final Future> undeploy(Context context) throws Exception {
@@ -240,7 +194,6 @@ protected void runOnContext(Handler action) {
vertxContext.runOnContext(action);
}
-
/**
* Executes blocking code asynchronously, returning a {@link Future} that completes
* when the blocking operation is done.
@@ -265,4 +218,165 @@ protected Future executeBlocking(Callable blockingCodeHandler) {
protected Future executeBlocking(Callable blockingCodeHandler, boolean ordered) {
return vertxContext.executeBlocking(blockingCodeHandler, ordered);
}
-}
\ No newline at end of file
+}
+
+// BosonVerticle for Vert.x 4.5.x
+// public abstract class BosonVerticle implements Verticle {
+// /**
+// * Reference to the Vert.x instance that deployed this verticle
+// */
+// protected Vertx vertx;
+//
+// /**
+// * Reference to the context of the verticle
+// */
+// protected Context vertxContext;
+//
+// /**
+// * Returns the Vert.x instance that deployed this Verticle.
+// *
+// * @return the Vert.x instance
+// */
+// public final Vertx getVertx() {
+// return vertx;
+// }
+//
+// /**
+// * Returns the Vert.x context associated with this Verticle.
+// *
+// * @return the Vert.x context
+// */
+// protected final Context vertxContext() {
+// return vertxContext;
+// }
+//
+// /**
+// * Returns the deployment ID of this Verticle deployment.
+// *
+// * @return the deployment ID
+// */
+// public final String deploymentID() {
+// return vertxContext.deploymentID();
+// }
+//
+// /**
+// * Returns the configuration object of this Verticle deployment.
+// *
+// * This configuration can be specified when the Verticle is deployed.
+// *
+// *
+// * @return the configuration as a {@link JsonObject}
+// */
+// protected final JsonObject vertxConfig() {
+// return vertxContext.config();
+// }
+//
+// /**
+// * Prepares this verticle for deployment.
+// *
+// * This method is invoked internally by {@link #init(Vertx, Context)} and can be overridden
+// * if additional setup is needed before deployment.
+// *
+// *
+// * @param vertx the Vert.x instance
+// * @param context the Vert.x context
+// */
+// protected void prepare(Vertx vertx, Context context) {
+// this.vertx = vertx;
+// this.vertxContext = context;
+// }
+//
+// /**
+// * Called during startup to perform asynchronous initialization logic.
+// *
+// * This method is invoked by {@link #start(Promise)}.
+// *
+// *
+// * @return a future that completes when setup is finished
+// */
+// protected abstract Future deploy();
+//
+// /**
+// * Called during shutdown to perform asynchronous cleanup logic.
+// *
+// * This method is invoked by {@link #stop(Promise)}.
+// *
+// *
+// * @return a future that completes when teardown is finished
+// */
+// protected abstract Future undeploy();
+//
+// /**
+// * Initialise the verticle with the Vert.x instance and the context.
+// *
+// * This method is called by Vert.x when the instance is deployed. You do not call it yourself.
+// *
+// * @param vertx the Vert.x instance
+// * @param context the context
+// */
+// public final void init(Vertx vertx, Context context) {
+// prepare(vertx, context);
+// }
+//
+// /**
+// * Start the verticle instance.
+// *
+// * Vert.x calls this method when deploying the instance. You do not call it yourself.
+// *
+// * A promise is passed into the method, and when deployment is complete the verticle should either call
+// * {@link io.vertx.core.Promise#complete} or {@link io.vertx.core.Promise#fail} the future.
+// *
+// * @param startPromise the future
+// */
+// public final void start(Promise startPromise) throws Exception {
+// deploy().onComplete(startPromise);
+// }
+//
+// /**
+// * Stop the verticle instance.
+// *
+// * Vert.x calls this method when un-deploying the instance. You do not call it yourself.
+// *
+// * A promise is passed into the method, and when un-deployment is complete the verticle should either call
+// * {@link io.vertx.core.Promise#complete} or {@link io.vertx.core.Promise#fail} the future.
+// *
+// * @param stopPromise the future
+// */
+// public final void stop(Promise stopPromise) throws Exception {
+// undeploy().onComplete(stopPromise);
+// }
+//
+// /**
+// * Executes the given handler on this verticle's context.
+// *
+// * @param action the handler to run
+// */
+// protected void runOnContext(Handler action) {
+// vertxContext.runOnContext(action);
+// }
+//
+// /**
+// * Executes blocking code asynchronously, returning a {@link Future} that completes
+// * when the blocking operation is done.
+// *
+// * @param blockingCodeHandler the blocking code to execute
+// * @param the result type
+// * @return a future representing the blocking operation result
+// */
+// protected Future executeBlocking(Callable blockingCodeHandler) {
+// return vertxContext.executeBlocking(blockingCodeHandler);
+// }
+//
+// /**
+// * Executes blocking code asynchronously, optionally ordering execution relative
+// * to other blocking operations in the same context.
+// *
+// * @param blockingCodeHandler the blocking code to execute
+// * @param ordered whether execution should be ordered
+// * @param the result type
+// * @return a future representing the blocking operation result
+// */
+// protected Future executeBlocking(Callable blockingCodeHandler, boolean ordered) {
+// return vertxContext.executeBlocking(blockingCodeHandler, ordered);
+// }
+//}
\ No newline at end of file
diff --git a/dht/src/main/java/io/bosonnetwork/kademlia/KadNode.java b/dht/src/main/java/io/bosonnetwork/kademlia/KadNode.java
index 610a4ed..365393d 100644
--- a/dht/src/main/java/io/bosonnetwork/kademlia/KadNode.java
+++ b/dht/src/main/java/io/bosonnetwork/kademlia/KadNode.java
@@ -221,14 +221,14 @@ public VertxFuture stop() {
}
@Override
- public void prepare(Vertx vertx, Context context) {
+ protected void prepare(Vertx vertx, Context context) {
super.prepare(vertx, context);
identity.initCache(VertxCaffeine.newBuilder(vertx)
.expireAfterAccess(KBucketEntry.OLD_AND_STALE_TIME, TimeUnit.MILLISECONDS));
}
@Override
- public Future deploy() {
+ protected Future deploy() {
tokenManager = new TokenManager();
String storageURI = config.databaseUri();
@@ -326,7 +326,7 @@ public void disconnected(Network network) {
}
@Override
- public Future undeploy() {
+ protected Future undeploy() {
running = false;
return Future.succeededFuture().andThen(ar -> {
diff --git a/dht/src/main/java/io/bosonnetwork/kademlia/impl/DHT.java b/dht/src/main/java/io/bosonnetwork/kademlia/impl/DHT.java
index 91f042a..cdc47fb 100644
--- a/dht/src/main/java/io/bosonnetwork/kademlia/impl/DHT.java
+++ b/dht/src/main/java/io/bosonnetwork/kademlia/impl/DHT.java
@@ -201,13 +201,13 @@ public void setConnectionStatusListener(ConnectionStatusListener listener) {
}
@Override
- public void prepare(Vertx vertx, Context context) {
+ protected void prepare(Vertx vertx, Context context) {
super.prepare(vertx, context);
this.kadContext = new KadContext(vertx, context, identity, network, this, enableDeveloperMode);
}
@Override
- public Future deploy() {
+ protected Future deploy() {
if (running)
return Future.succeededFuture();
@@ -279,7 +279,7 @@ public Future deploy() {
}
@Override
- public Future undeploy() {
+ protected Future undeploy() {
if (!running)
return Future.succeededFuture();
diff --git a/dht/src/test/java/io/bosonnetwork/kademlia/rpc/RPCServerTests.java b/dht/src/test/java/io/bosonnetwork/kademlia/rpc/RPCServerTests.java
index bc597c3..0b97ebe 100644
--- a/dht/src/test/java/io/bosonnetwork/kademlia/rpc/RPCServerTests.java
+++ b/dht/src/test/java/io/bosonnetwork/kademlia/rpc/RPCServerTests.java
@@ -123,7 +123,7 @@ public Network getNetwork() {
}
@Override
- public void prepare(Vertx vertx, Context context) {
+ protected void prepare(Vertx vertx, Context context) {
super.prepare(vertx, context);
kadContext = new KadContext(vertx, context, identity, getNetwork(), null);
@@ -133,12 +133,12 @@ public void prepare(Vertx vertx, Context context) {
}
@Override
- public Future deploy() {
+ protected Future deploy() {
return rpcServer.start();
}
@Override
- public Future undeploy() {
+ protected Future undeploy() {
if (rpcServer != null)
return rpcServer.stop().andThen(ar -> rpcServer = null);
else