From 29d4ad6e8cfbc2fd7febbc5581f172336131f0de Mon Sep 17 00:00:00 2001 From: Jingyu Date: Tue, 9 Dec 2025 23:27:21 +0800 Subject: [PATCH] Update core APIs for Boson Director integration and add Javadoc --- accesscontrol/pom.xml | 7 +- .../DefaultNodeConfiguration.java | 569 +++++++++--------- .../java/io/bosonnetwork/NodeBlacklist.java | 26 + .../io/bosonnetwork/NodeConfiguration.java | 10 +- .../java/io/bosonnetwork/database/Filter.java | 47 +- .../io/bosonnetwork/database/Ordering.java | 51 +- .../io/bosonnetwork/database/Pagination.java | 26 +- .../io/bosonnetwork/metrics/Measured.java | 22 + .../java/io/bosonnetwork/metrics/Metrics.java | 22 + .../io/bosonnetwork/service/BosonService.java | 43 +- .../service/ClientAuthenticator.java | 114 ++++ .../service/ClientAuthorizer.java | 60 ++ .../io/bosonnetwork/service/ClientDevice.java | 68 +++ .../io/bosonnetwork/service/ClientUser.java | 84 +++ .../java/io/bosonnetwork/service/Clients.java | 73 ++- .../service/DefaultServiceContext.java | 135 ++++- .../bosonnetwork/service/FederatedNode.java | 103 ++++ .../service/FederatedService.java | 27 - .../io/bosonnetwork/service/Federation.java | 73 ++- .../service/FederationAuthenticator.java | 135 +++++ .../bosonnetwork/service/ServiceContext.java | 46 +- .../io/bosonnetwork/service/ServiceInfo.java | 89 +++ .../io/bosonnetwork/utils/AddressUtils.java | 9 +- .../bosonnetwork/utils/ApplicationLock.java | 22 + .../java/io/bosonnetwork/utils/ConfigMap.java | 545 +++++++++++++++++ .../java/io/bosonnetwork/utils/FileUtils.java | 198 +++++- .../main/java/io/bosonnetwork/utils/Json.java | 6 + .../HighlightingCompositeConverter.java | 22 + .../io/bosonnetwork/vertx/BosonVerticle.java | 10 - .../io/bosonnetwork/vertx/VertxFuture.java | 4 +- .../bosonnetwork/vertx/VertxFutureTests.java | 22 +- ...{1_init_schema.sql => 001_init_schema.sql} | 0 .../{2_add_index.sql => 002_add_index.sql} | 0 ...le_data.sql => 003_insert_sample_data.sql} | 0 ...le_table.sql => 004_add_profile_table.sql} | 0 ...lumn.sql => 005_add_last_login_column.sql} | 0 ..._case.sql => 006_normalize_email_case.sql} | 0 ...le.sql => 007_add_message_likes_table.sql} | 0 ...ql => 008_add_view_user_messages_view.sql} | 0 ..._table.sql => 009_add_audit_log_table.sql} | 0 ...10_add_trigger.sql => 010_add_trigger.sql} | 0 ...{1_init_schema.sql => 001_init_schema.sql} | 0 .../{2_add_index.sql => 002_add_index.sql} | 0 ...le_data.sql => 003_insert_sample_data.sql} | 0 ...le_table.sql => 004_add_profile_table.sql} | 0 ...lumn.sql => 005_add_last_login_column.sql} | 0 ..._case.sql => 006_normalize_email_case.sql} | 0 ...le.sql => 007_add_message_likes_table.sql} | 0 ...ql => 008_add_view_user_messages_view.sql} | 0 ..._table.sql => 009_add_audit_log_table.sql} | 0 ...10_add_trigger.sql => 010_add_trigger.sql} | 0 .../java/io/bosonnetwork/am/AmCommand.java | 7 +- .../java/io/bosonnetwork/launcher/Main.java | 24 +- .../main/java/io/bosonnetwork/shell/Main.java | 30 +- .../io/bosonnetwork/kademlia/KadNode.java | 57 +- .../io/bosonnetwork/kademlia/impl/DHT.java | 9 +- .../impl/SimpleNodeConfiguration.java | 18 +- ...tial_schema.sql => 001_initial_schema.sql} | 0 ...tial_schema.sql => 001_initial_schema.sql} | 0 dht/src/main/resources/node.yaml | 105 ++++ .../bosonnetwork/kademlia/NodeAsyncTests.java | 96 +-- .../bosonnetwork/kademlia/NodeSyncTests.java | 17 +- .../io/bosonnetwork/kademlia/SybilTests.java | 15 +- .../kademlia/rpc/RPCServerTests.java | 9 +- 64 files changed, 2488 insertions(+), 567 deletions(-) create mode 100644 api/src/main/java/io/bosonnetwork/service/ClientAuthorizer.java delete mode 100644 api/src/main/java/io/bosonnetwork/service/FederatedService.java create mode 100644 api/src/main/java/io/bosonnetwork/service/ServiceInfo.java create mode 100644 api/src/main/java/io/bosonnetwork/utils/ConfigMap.java rename api/src/test/resources/db/postgres/{1_init_schema.sql => 001_init_schema.sql} (100%) rename api/src/test/resources/db/postgres/{2_add_index.sql => 002_add_index.sql} (100%) rename api/src/test/resources/db/postgres/{3_insert_sample_data.sql => 003_insert_sample_data.sql} (100%) rename api/src/test/resources/db/postgres/{4_add_profile_table.sql => 004_add_profile_table.sql} (100%) rename api/src/test/resources/db/postgres/{5_add_last_login_column.sql => 005_add_last_login_column.sql} (100%) rename api/src/test/resources/db/postgres/{6_normalize_email_case.sql => 006_normalize_email_case.sql} (100%) rename api/src/test/resources/db/postgres/{7_add_message_likes_table.sql => 007_add_message_likes_table.sql} (100%) rename api/src/test/resources/db/postgres/{8_add_view_user_messages_view.sql => 008_add_view_user_messages_view.sql} (100%) rename api/src/test/resources/db/postgres/{9_add_audit_log_table.sql => 009_add_audit_log_table.sql} (100%) rename api/src/test/resources/db/postgres/{10_add_trigger.sql => 010_add_trigger.sql} (100%) rename api/src/test/resources/db/sqlite/{1_init_schema.sql => 001_init_schema.sql} (100%) rename api/src/test/resources/db/sqlite/{2_add_index.sql => 002_add_index.sql} (100%) rename api/src/test/resources/db/sqlite/{3_insert_sample_data.sql => 003_insert_sample_data.sql} (100%) rename api/src/test/resources/db/sqlite/{4_add_profile_table.sql => 004_add_profile_table.sql} (100%) rename api/src/test/resources/db/sqlite/{5_add_last_login_column.sql => 005_add_last_login_column.sql} (100%) rename api/src/test/resources/db/sqlite/{6_normalize_email_case.sql => 006_normalize_email_case.sql} (100%) rename api/src/test/resources/db/sqlite/{7_add_message_likes_table.sql => 007_add_message_likes_table.sql} (100%) rename api/src/test/resources/db/sqlite/{8_add_view_user_messages_view.sql => 008_add_view_user_messages_view.sql} (100%) rename api/src/test/resources/db/sqlite/{9_add_audit_log_table.sql => 009_add_audit_log_table.sql} (100%) rename api/src/test/resources/db/sqlite/{10_add_trigger.sql => 010_add_trigger.sql} (100%) rename dht/src/main/resources/db/postgres/{1_initial_schema.sql => 001_initial_schema.sql} (100%) rename dht/src/main/resources/db/sqlite/{1_initial_schema.sql => 001_initial_schema.sql} (100%) create mode 100644 dht/src/main/resources/node.yaml diff --git a/accesscontrol/pom.xml b/accesscontrol/pom.xml index 3822d9f..b1834bd 100644 --- a/accesscontrol/pom.xml +++ b/accesscontrol/pom.xml @@ -133,11 +133,6 @@ maven-source-plugin - - org.apache.maven.plugins - maven-javadoc-plugin - - org.apache.maven.plugins maven-gpg-plugin @@ -149,4 +144,4 @@ - + \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/DefaultNodeConfiguration.java b/api/src/main/java/io/bosonnetwork/DefaultNodeConfiguration.java index cf4ba73..315829b 100644 --- a/api/src/main/java/io/bosonnetwork/DefaultNodeConfiguration.java +++ b/api/src/main/java/io/bosonnetwork/DefaultNodeConfiguration.java @@ -25,36 +25,40 @@ import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; -import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import com.fasterxml.jackson.databind.ObjectMapper; import io.vertx.core.Vertx; import io.bosonnetwork.crypto.Signature; import io.bosonnetwork.utils.AddressUtils; import io.bosonnetwork.utils.Base58; -import io.bosonnetwork.utils.Json; +import io.bosonnetwork.utils.ConfigMap; +import io.bosonnetwork.utils.Hex; /** * Default configuration implementation for the {@link NodeConfiguration} interface. + *

+ * Use the {@link Builder} class to construct instances with a fluent API. The configuration + * can also be serialized to/from Map for persistence or network transmission. + *

+ * + * @see NodeConfiguration + * @see Builder */ -@JsonPropertyOrder({"host4", "host6", "port", "privateKey", "dataPath", "storageURL", "bootstraps", - "spamThrottling", "suspiciousNodeDetector", "developerMode", "metrics"}) public class DefaultNodeConfiguration implements NodeConfiguration { /** * The default port for the DHT node, chosen from the IANA unassigned range (38866-39062). @@ -71,75 +75,56 @@ public class DefaultNodeConfiguration implements NodeConfiguration { /** * IPv4 address string for the DHT node. If null or empty, disables DHT on IPv4. */ - @JsonProperty("host4") - @JsonInclude(JsonInclude.Include.NON_EMPTY) - String host4; + private String host4; /** * IPv6 address string for the DHT node. If null or empty, disables DHT on IPv6. */ - @JsonProperty("host6") - @JsonInclude(JsonInclude.Include.NON_EMPTY) - String host6; + private String host6; /** * The port number for the DHT node. */ - @JsonProperty("port") - @JsonInclude(JsonInclude.Include.NON_EMPTY) - int port; + private int port; /** * The node's private key, encoded in Base58. */ - @JsonProperty("privateKey") - @JsonInclude(JsonInclude.Include.NON_EMPTY) - String privateKey; + private Signature.PrivateKey privateKey; /** - * Path to the directory for persistent DHT data storage. If null, disables persistence. + * Path to the directory for persistent DHT data storage. disables persistence if null. */ - private Path dataPath; + private Path dataDir; /** - * Optional external storage URL for the node. + * Optional external storage URI for the node. */ - @JsonProperty("storageURL") - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private String storageURL; + private String storageURI; /** * Set of bootstrap nodes for joining the DHT network. */ - @JsonProperty("bootstraps") - @JsonInclude(JsonInclude.Include.NON_EMPTY) private final Set bootstraps; /** * Whether spam throttling is enabled for this node. */ - @JsonProperty("spamThrottling") - @JsonInclude(JsonInclude.Include.NON_DEFAULT) private boolean enableSpamThrottling; /** * Whether suspicious node detection is enabled for this node. */ - @JsonProperty("suspiciousNodeDetector") - @JsonInclude(JsonInclude.Include.NON_DEFAULT) private boolean enableSuspiciousNodeDetector; /** * Whether developer mode is enabled for this node. */ - @JsonProperty("developerMode") - @JsonInclude(JsonInclude.Include.NON_DEFAULT) private boolean enableDeveloperMode; /** - * Whether metrics collection is enabled for this node. + * Whether metrics is enabled for this node. */ - @JsonProperty("metrics") private boolean enableMetrics; /** @@ -148,12 +133,14 @@ public class DefaultNodeConfiguration implements NodeConfiguration { * developer mode and metrics are disabled, and no bootstraps are set. */ private DefaultNodeConfiguration() { - this.bootstraps = new HashSet<>(); + this.port = DEFAULT_DHT_PORT; + this.storageURI = "jdbc:sqlite:node.db"; this.enableSpamThrottling = true; this.enableSuspiciousNodeDetector = true; this.enableDeveloperMode = false; this.enableMetrics = false; - this.port = DEFAULT_DHT_PORT; + + this.bootstraps = new HashSet<>(); } /** @@ -165,6 +152,19 @@ public Vertx vertx() { return vertx; } + /** + * Sets the Vert.x instance for this configuration. + *

+ * This method is typically used internally to inject the Vert.x instance after + * configuration construction. + *

+ * + * @param vertx the Vert.x instance to set + */ + public void setVertx(Vertx vertx) { + this.vertx = vertx; + } + /** * {@inheritDoc} * @return the IPv4 address string for the DHT node, or null if disabled. @@ -197,7 +197,7 @@ public int port() { * @return the Base58-encoded private key string. */ @Override - public String privateKey() { + public Signature.PrivateKey privateKey() { return privateKey; } @@ -206,27 +206,8 @@ public String privateKey() { * @return the path to the persistent data directory, or null if persistence is disabled. */ @Override - public Path dataPath() { - return dataPath; - } - - /** - * For Jackson serialization: gets the string representation of the data path. - * @return the string path, or null if not set. - */ - @JsonProperty("dataPath") - @JsonInclude(JsonInclude.Include.NON_EMPTY) - private String getDataPath() { - return dataPath != null ? dataPath.toString() : null; - } - - /** - * For Jackson deserialization: sets the data path from a string. - * @param dataPath the string path to set (may be null). - */ - @JsonProperty("dataPath") - private void setDataPath(String dataPath) { - this.dataPath = normalizePath(dataPath != null ? Path.of(dataPath) : null); + public Path dataDir() { + return dataDir; } /** @@ -234,8 +215,8 @@ private void setDataPath(String dataPath) { * @return the external storage URL, or null if not set. */ @Override - public String storageURL() { - return storageURL; + public String storageURI() { + return storageURI; } /** @@ -276,13 +257,137 @@ public boolean enableDeveloperMode() { /** * {@inheritDoc} - * @return true if metrics collection is enabled. + * @return true if metrics is enabled. */ @Override public boolean enableMetrics() { return enableMetrics; } + /** + * Creates a DefaultNodeConfiguration from a Map representation. + *

+ * This static factory method deserializes a configuration from a Map structure. + * The map should contain the following keys: + *

+ * + * @param map the map containing configuration data, must not be null or empty + * @return a new DefaultNodeConfiguration instance + * @throws NullPointerException if map is null + * @throws IllegalArgumentException if map is empty, required fields are missing, or values are invalid + */ + public static DefaultNodeConfiguration fromMap(Map map) { + Objects.requireNonNull(map, "map"); + if (map.isEmpty()) + throw new IllegalArgumentException("Configuration is empty"); + + DefaultNodeConfiguration config = new DefaultNodeConfiguration(); + + ConfigMap m = new ConfigMap(map); + + config.host4 = m.getString("host4", config.host4); + config.host6 = m.getString("host6", config.host6); + if (config.host4 == null || config.host4.isEmpty() && config.host6 == null || config.host6.isEmpty()) + throw new IllegalArgumentException("Missing host4 or host6"); + + config.port = m.getPort("port", config.port); + String sk = m.getString("privateKey", null); + if (sk == null || sk.isEmpty()) + throw new IllegalArgumentException("Missing privateKey"); + try { + byte[] keyBytes = sk.startsWith("0x") ? Hex.decode(sk.substring(2)) : Base58.decode(sk); + config.privateKey = Signature.PrivateKey.fromBytes(keyBytes); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid privateKey: " + config.privateKey); + } + + String dir = m.getString("dataDir", null); + if (dir != null && !dir.isEmpty()) + config.dataDir = Path.of(dir); + + config.storageURI = m.getString("storageURI", config.storageURI); + if (config.storageURI == null || config.storageURI.isEmpty()) + throw new IllegalArgumentException("Missing storageURI"); + + List> lst = m.getList("bootstraps"); + if (lst != null && !lst.isEmpty()) { + lst.forEach(b -> { + if (b.size() != 3) + throw new IllegalArgumentException("Invalid bootstrap node: missing fields - " + b); + + try { + Id id = Id.of((String) b.get(0)); + String host = (String) b.get(1); + int port = (int) b.get(2); + + config.bootstraps.add(new NodeInfo(id, host, port)); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid bootstrap node: " + b); + } + }); + } + + config.enableSpamThrottling = m.getBoolean("enableSpamThrottling", config.enableSpamThrottling); + config.enableSuspiciousNodeDetector = m.getBoolean("enableSuspiciousNodeDetector", config.enableSuspiciousNodeDetector); + config.enableDeveloperMode = m.getBoolean("enableDeveloperMode", config.enableDeveloperMode); + config.enableMetrics = m.getBoolean("enableMetrics", config.enableMetrics); + + return config; + } + + /** + * Serializes this configuration to a Map representation. + *

+ * The returned map contains all configured values and can be used for persistence, + * network transmission, or creating a new configuration via {@link #fromMap(Map)}. + * Null or empty values are excluded from the map. + *

+ * + * @return a Map containing the configuration data + */ + public Map toMap() { + HashMap map = new HashMap<>(); + + if (host4 != null) + map.put("host4", host4); + + if (host6 != null) + map.put("host6", host6); + + map.put("port", port); + map.put("privateKey", privateKey); + + if (dataDir != null) + map.put("dataDir", dataDir); + + map.put("storageURI", storageURI); + + if (!bootstraps.isEmpty()) { + List> lst = new ArrayList<>(); + bootstraps.forEach(n -> lst.add(Arrays.asList(n.getId().toString(), n.getHost(), n.getPort()))); + map.put("bootstraps", lst); + } + + map.put("enableSpamThrottling", enableSpamThrottling); + map.put("enableSuspiciousNodeDetector", enableSuspiciousNodeDetector); + map.put("enableDeveloperMode", enableDeveloperMode); + map.put("enableMetrics", enableMetrics); + + return map; + } + /** * Builder helper class to create a {@link NodeConfiguration} object. *

@@ -297,7 +402,39 @@ public static class Builder { * Constructs a new Builder with default settings. */ protected Builder() { - reset(); + config = new DefaultNodeConfiguration(); + } + + /** + * Gets or lazily initializes the configuration instance. + *

+ * This private helper ensures the config field is initialized on first access. + *

+ * + * @return the configuration instance + */ + private DefaultNodeConfiguration config() { + return config == null ? config = new DefaultNodeConfiguration() : config; + } + + /** + * Initializes this builder from a template Map. + *

+ * This method loads a complete configuration from a Map, replacing any previously + * set values. The template map should follow the same structure as expected by + * {@link DefaultNodeConfiguration#fromMap(Map)}. + *

+ * + * @param template the template map containing configuration data, must not be null + * @return this Builder for chaining + * @throws NullPointerException if template is null + * @throws IllegalArgumentException if the template is invalid + * @see DefaultNodeConfiguration#fromMap(Map) + */ + public Builder template(Map template) { + Objects.requireNonNull(template, "template"); + this.config = DefaultNodeConfiguration.fromMap(template); + return this; } /** @@ -308,7 +445,7 @@ protected Builder() { */ public Builder vertx(Vertx vertx) { Objects.requireNonNull(vertx, "vertx"); - config.vertx = vertx; + config().vertx = vertx; return this; } @@ -318,16 +455,11 @@ public Builder vertx(Vertx vertx) { * @throws IllegalStateException if no suitable IPv4 address is found */ public Builder autoHost4() { - InetAddress addr = AddressUtils.getAllAddresses() - .filter(Inet4Address.class::isInstance) - .filter(AddressUtils::isAnyUnicast) - .distinct() - .findFirst() - .orElse(null); + InetAddress addr = AddressUtils.getDefaultRouteAddress(Inet6Address.class); if (addr == null) throw new IllegalStateException("No available IPv4 address"); - config.host4 = addr.getHostAddress(); + config().host4 = addr.getHostAddress(); return this; } @@ -337,16 +469,11 @@ public Builder autoHost4() { * @throws IllegalStateException if no suitable IPv6 address is found */ public Builder autoHost6() { - InetAddress addr = AddressUtils.getAllAddresses() - .filter(Inet6Address.class::isInstance) - .filter(AddressUtils::isAnyUnicast) - .distinct() - .findFirst() - .orElse(null); + InetAddress addr = AddressUtils.getDefaultRouteAddress(Inet6Address.class); if (addr == null) throw new IllegalStateException("No available IPv6 address"); - config.host6 = addr.getHostAddress(); + config().host6 = addr.getHostAddress(); return this; } @@ -356,28 +483,28 @@ public Builder autoHost6() { * @throws IllegalStateException if neither IPv4 nor IPv6 addresses are found */ public Builder autoHosts() { - InetAddress addr4 = AddressUtils.getAllAddresses() - .filter(Inet4Address.class::isInstance) - .filter(AddressUtils::isAnyUnicast) - .distinct() - .findFirst() - .orElse(null); - - InetAddress addr6 = AddressUtils.getAllAddresses() - .filter(Inet6Address.class::isInstance) - .filter(AddressUtils::isAnyUnicast) - .distinct() - .findFirst() - .orElse(null); + InetAddress addr4; + try { + addr4 = AddressUtils.getDefaultRouteAddress(Inet4Address.class); + } catch (Exception e) { + addr4 = null; + } + + InetAddress addr6; + try { + addr6 = AddressUtils.getDefaultRouteAddress(Inet6Address.class); + } catch (Exception e) { + addr6 = null; + } if (addr4 == null && addr6 == null) throw new IllegalStateException("No available IPv4/6 address"); if (addr4 != null) - config.host4 = addr4.getHostAddress(); + config().host4 = addr4.getHostAddress(); if (addr6 != null) - config.host6 = addr6.getHostAddress(); + config().host6 = addr6.getHostAddress(); return this; } @@ -387,7 +514,7 @@ public Builder autoHosts() { * @param host the string host name or IPv4 address (must not be null) * @return this Builder for chaining * @throws IllegalArgumentException if the host is not a valid IPv4 address - * @throws NullPointerException if host is null + * @throws NullPointerException if the host is null */ public Builder host4(String host) { Objects.requireNonNull(host, "host"); @@ -412,7 +539,7 @@ public Builder address4(InetAddress addr) { throw new IllegalArgumentException("Not any unicast address"); if (addr instanceof Inet4Address) - config.host4 = addr.getHostAddress(); + config().host4 = addr.getHostAddress(); else throw new IllegalArgumentException("Invalid IPv4 address: " + addr); @@ -424,7 +551,7 @@ public Builder address4(InetAddress addr) { * @param host the string host name or IPv6 address (must not be null) * @return this Builder for chaining * @throws IllegalArgumentException if the host is not a valid IPv6 address - * @throws NullPointerException if host is null + * @throws NullPointerException if the host is null */ public Builder host6(String host) { Objects.requireNonNull(host, "host"); @@ -449,7 +576,7 @@ public Builder address6(InetAddress addr) { throw new IllegalArgumentException("Not any unicast address"); if (addr instanceof Inet6Address) - config.host6 = addr.getHostAddress(); + config().host6 = addr.getHostAddress(); else throw new IllegalArgumentException("Invalid IPv6 address: " + addr); @@ -466,7 +593,7 @@ public Builder port(int port) { if (port <= 0 || port > 65535) throw new IllegalArgumentException("Invalid port: " + port); - config.port = port; + config().port = port; return this; } @@ -475,7 +602,7 @@ public Builder port(int port) { * @return this Builder for chaining */ public Builder generatePrivateKey() { - config.privateKey = Base58.encode(Signature.KeyPair.random().privateKey().bytes()); + config().privateKey = Signature.KeyPair.random().privateKey(); return this; } @@ -486,28 +613,24 @@ public Builder generatePrivateKey() { * @throws IllegalArgumentException if the key is not 64 bytes */ public Builder privateKey(byte[] privateKey) { - if (privateKey == null || privateKey.length != 64) - throw new IllegalArgumentException("Invalid private key"); - - config.privateKey = Base58.encode(privateKey); + Objects.requireNonNull(privateKey, "privateKey"); + config().privateKey = Signature.PrivateKey.fromBytes(privateKey); return this; } /** * Set the node's private key from a Base58-encoded string. - * @param privateKey the Base58-encoded private key string (must not be null) + * @param privateKey the Base58-encoded or hex-encoded private key string (must not be null) * @return this Builder for chaining * @throws IllegalArgumentException if the key is not 64 bytes when decoded * @throws NullPointerException if privateKey is null */ public Builder privateKey(String privateKey) { Objects.requireNonNull(privateKey, "privateKey"); - - byte[] key = Base58.decode(privateKey); - if (key.length != 64) - throw new IllegalArgumentException("Invalid private key"); - - config.privateKey = privateKey; + byte[] key = privateKey.startsWith("0x") ? + Hex.decode(privateKey, 2, privateKey.length() - 2) : + Base58.decode(privateKey); + config().privateKey = Signature.PrivateKey.fromBytes(key); return this; } @@ -516,35 +639,35 @@ public Builder privateKey(String privateKey) { * @return true if a private key is set, false otherwise */ public boolean hasPrivateKey() { - return config.privateKey != null; + return config().privateKey != null; } /** * Set the storage path for DHT persistent data using a string path. - * @param path the string path (may be null to disable persistence) + * @param dir the string path (maybe null to disable persistence) * @return this Builder for chaining */ - public Builder dataPath(String path) { - return dataPath(path != null ? Path.of(path) : null); + public Builder dataDir(String dir) { + return dataDir(dir != null ? Path.of(dir) : null); } /** * Set the storage path for DHT persistent data using a File object. - * @param path the File pointing to the storage directory (may be null to disable persistence) + * @param path the File pointing to the storage directory (maybe null to disable persistence) * @return this Builder for chaining */ - public Builder dataPath(File path) { - dataPath(path != null ? path.toPath() : null); + public Builder dataDir(File path) { + dataDir(path != null ? path.toPath() : null); return this; } /** * Set the storage path for DHT persistent data using a Path. - * @param path the Path to the storage directory (may be null to disable persistence) + * @param path the Path to the storage directory (maybe null to disable persistence) * @return this Builder for chaining */ - public Builder dataPath(Path path) { - config.dataPath = normalizePath(path); + public Builder dataDir(Path path) { + config().dataDir = path; return this; } @@ -552,27 +675,29 @@ public Builder dataPath(Path path) { * Checks if a data path has been set. * @return true if a data path is set, false otherwise */ - public boolean hasDataPath() { - return config.dataPath != null; + public boolean hasDataDir() { + return config().dataDir != null; } /** * Gets the current data path set in the builder. * @return the Path to the storage directory, or null if not set */ - public Path dataPath() { - return config.dataPath; + public Path dataDir() { + return config().dataDir; } /** * Set the external storage URL for the node. - * @param storageURL the storage URL (must not be null) + * @param storageURI the storage URL (must not be null) * @return this Builder for chaining - * @throws NullPointerException if storageURL is null + * @throws NullPointerException if storageURI is null */ - public Builder storageURL(String storageURL) { - Objects.requireNonNull(storageURL, "storageURL"); - config.storageURL = storageURL; + public Builder storageURI(String storageURI) { + Objects.requireNonNull(storageURI, "storageURI"); + if (!storageURI.startsWith("postgresql://") && !storageURI.startsWith("jdbc:sqlite:")) + throw new IllegalArgumentException("Unsupported storage URL: " + storageURI); + config().storageURI = storageURI; return this; } @@ -585,7 +710,7 @@ public Builder storageURL(String storageURL) { */ public Builder addBootstrap(String id, String addr, int port) { NodeInfo node = new NodeInfo(Id.of(id), addr, port); - config.bootstraps.add(node); + config().bootstraps.add(node); return this; } @@ -598,7 +723,7 @@ public Builder addBootstrap(String id, String addr, int port) { */ public Builder addBootstrap(Id id, InetAddress addr, int port) { NodeInfo node = new NodeInfo(id, addr, port); - config.bootstraps.add(node); + config().bootstraps.add(node); return this; } @@ -610,7 +735,7 @@ public Builder addBootstrap(Id id, InetAddress addr, int port) { */ public Builder addBootstrap(Id id, InetSocketAddress addr) { NodeInfo node = new NodeInfo(id, addr); - config.bootstraps.add(node); + config().bootstraps.add(node); return this; } @@ -618,11 +743,11 @@ public Builder addBootstrap(Id id, InetSocketAddress addr) { * Add a new bootstrap node to the configuration. * @param node the NodeInfo of the bootstrap node (must not be null) * @return this Builder for chaining - * @throws NullPointerException if node is null + * @throws NullPointerException if the node is null */ public Builder addBootstrap(NodeInfo node) { Objects.requireNonNull(node, "node"); - config.bootstraps.add(node); + config().bootstraps.add(node); return this; } @@ -630,11 +755,11 @@ public Builder addBootstrap(NodeInfo node) { * Add multiple bootstrap nodes to the configuration. * @param nodes the collection of NodeInfo bootstrap nodes (must not be null) * @return this Builder for chaining - * @throws NullPointerException if nodes is null + * @throws NullPointerException if the nodes parameter is null */ public Builder addBootstrap(Collection nodes) { Objects.requireNonNull(nodes, "nodes"); - config.bootstraps.addAll(nodes); + config().bootstraps.addAll(nodes); return this; } @@ -643,7 +768,7 @@ public Builder addBootstrap(Collection nodes) { * @return this Builder for chaining */ public Builder enableSpamThrottling() { - config.enableSpamThrottling = true; + config().enableSpamThrottling = true; return this; } @@ -652,7 +777,7 @@ public Builder enableSpamThrottling() { * @return this Builder for chaining */ public Builder disableSpamThrottling() { - config.enableSpamThrottling = false; + config().enableSpamThrottling = false; return this; } @@ -661,7 +786,7 @@ public Builder disableSpamThrottling() { * @return this Builder for chaining */ public Builder enableSuspiciousNodeDetector() { - config.enableSuspiciousNodeDetector = true; + config().enableSuspiciousNodeDetector = true; return this; } @@ -670,7 +795,7 @@ public Builder enableSuspiciousNodeDetector() { * @return this Builder for chaining */ public Builder disableSuspiciousNodeDetector() { - config.enableSuspiciousNodeDetector = false; + config().enableSuspiciousNodeDetector = false; return this; } @@ -679,7 +804,7 @@ public Builder disableSuspiciousNodeDetector() { * @return this Builder for chaining */ public Builder enableDeveloperMode() { - config.enableDeveloperMode = true; + config().enableDeveloperMode = true; return this; } @@ -688,136 +813,20 @@ public Builder enableDeveloperMode() { * @return this Builder for chaining */ public Builder disableDeveloperMode() { - config.enableDeveloperMode = false; - return this; - } - - /** - * Enables metrics collection for the node. - * @return this Builder for chaining - */ - public Builder enableMetrics() { - config.enableMetrics = true; - return this; - } - - /** - * Disables metrics collection for the node. - * @return this Builder for chaining - */ - public Builder disableMetrics() { - config.enableMetrics = false; + config().enableDeveloperMode = false; return this; } /** - * Loads the configuration data from a JSON or YAML file. - * The format is determined by the file extension (.json for JSON, otherwise YAML). - * @param file the string file path to load (must not be null) + * Enables metrics for the node. + * @param enable true to enable metrics, false to disable * @return this Builder for chaining - * @throws IOException if I/O error occurs during loading - * @throws IllegalArgumentException if the file does not exist or is a directory */ - public Builder load(String file) throws IOException { - Objects.requireNonNull(file, "file"); - Path configFile = Path.of(file); - return load(configFile); - } - - /** - * Loads the configuration data from a JSON or YAML file. - * The format is determined by the file extension (.json for JSON, otherwise YAML). - * @param file the File to load (must not be null) - * @return this Builder for chaining - * @throws IOException if I/O error occurs during loading - * @throws IllegalArgumentException if the file does not exist or is a directory - */ - public Builder load(File file) throws IOException { - Objects.requireNonNull(file, "file"); - return load(file.toPath()); - } - - /** - * Loads the configuration data from a JSON or YAML file. - * The format is determined by the file extension (.json for JSON, otherwise YAML). - * @param file the Path to the file to load (must not be null) - * @return this Builder for chaining - * @throws IOException if I/O error occurs during loading - * @throws IllegalArgumentException if the file does not exist or is a directory - */ - public Builder load(Path file) throws IOException { - Objects.requireNonNull(file, "file"); - file = normalizePath(file); - if (Files.notExists(file) || Files.isDirectory(file)) - throw new IllegalArgumentException("Invalid config file: " + file); - - try (InputStream in = Files.newInputStream(file)) { - ObjectMapper mapper = file.getFileName().toString().endsWith(".json") ? - Json.objectMapper() : Json.yamlMapper(); - config = mapper.readValue(in, DefaultNodeConfiguration.class); - } - + public Builder enableMetrics(boolean enable) { + config().enableMetrics = enable; return this; } - /** - * Saves the current configuration to a file in JSON or YAML format. - * The format is determined by the file extension (.json for JSON, otherwise YAML). - * @param file the string file path to save to (must not be null) - * @return this Builder for chaining - * @throws IOException if I/O error occurs during saving - * @throws IllegalArgumentException if the file path is a directory - */ - public Builder save(String file) throws IOException { - Objects.requireNonNull(file, "file"); - return save(Path.of(file)); - } - - /** - * Saves the current configuration to a file in JSON or YAML format. - * The format is determined by the file extension (.json for JSON, otherwise YAML). - * @param file the File to save to (must not be null) - * @return this Builder for chaining - * @throws IOException if I/O error occurs during saving - * @throws IllegalArgumentException if the file path is a directory - */ - public Builder save(File file) throws IOException { - Objects.requireNonNull(file, "file"); - return save(file.toPath()); - } - - /** - * Saves the current configuration to a file in JSON or YAML format. - * The format is determined by the file extension (.json for JSON, otherwise YAML). - * @param file the Path to save to (must not be null) - * @return this Builder for chaining - * @throws IOException if I/O error occurs during saving - * @throws IllegalArgumentException if the file path is a directory - */ - public Builder save(Path file) throws IOException { - Objects.requireNonNull(file, "file"); - file = normalizePath(file); - if (Files.exists(file) && Files.isDirectory(file)) - throw new IllegalArgumentException("Invalid config file: " + file); - - Files.createDirectories(file.getParent()); - try (OutputStream out = Files.newOutputStream(file)) { - ObjectMapper mapper = file.getFileName().toString().endsWith(".json") ? - Json.objectMapper() : Json.yamlMapper(); - mapper.writeValue(out, config); - } - - return this; - } - - /** - * Resets the configuration builder object to the initial state, - * clearing all existing settings. - */ - private void reset() { - config = new DefaultNodeConfiguration(); - } - /** * Creates the {@link NodeConfiguration} instance with the current settings in this builder. * After creating the new {@link NodeConfiguration} instance, the builder will be reset to the @@ -825,31 +834,15 @@ private void reset() { * @return the {@link NodeConfiguration} instance */ public NodeConfiguration build() { - if (config.privateKey == null) - config.privateKey = Base58.encode(Signature.KeyPair.random().privateKey().bytes()); + if (config().host4() == null && config().host6() == null) + throw new IllegalArgumentException("Missing host4 or host6"); + + if (config().privateKey == null) + generatePrivateKey(); - DefaultNodeConfiguration c = config; - reset(); + DefaultNodeConfiguration c = config(); + config = null; return c; } } - - /** - * Normalizes a filesystem path, expanding '~' to the user's home directory if present, - * and converting to an absolute path. - * @param path the path to normalize (may be null) - * @return the normalized absolute path, or null if input is null - */ - private static Path normalizePath(Path path) { - if (path == null) - return null; - - path = path.normalize(); - if (path.startsWith("~")) - path = Path.of(System.getProperty("user.home")).resolve(path.subpath(1, path.getNameCount())); - else - path = path.toAbsolutePath(); - - return path; - } } \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/NodeBlacklist.java b/api/src/main/java/io/bosonnetwork/NodeBlacklist.java index de838d5..654422e 100644 --- a/api/src/main/java/io/bosonnetwork/NodeBlacklist.java +++ b/api/src/main/java/io/bosonnetwork/NodeBlacklist.java @@ -1,5 +1,31 @@ +/* + * 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; +/** + * Provides an interface for managing a blacklist of nodes, allowing hosts and IDs + * to be banned and checked for their banned status. + */ public interface NodeBlacklist { /** * Checks if the specified host is banned. diff --git a/api/src/main/java/io/bosonnetwork/NodeConfiguration.java b/api/src/main/java/io/bosonnetwork/NodeConfiguration.java index 4eaa906..9cafe7e 100644 --- a/api/src/main/java/io/bosonnetwork/NodeConfiguration.java +++ b/api/src/main/java/io/bosonnetwork/NodeConfiguration.java @@ -29,6 +29,8 @@ import io.vertx.core.Vertx; +import io.bosonnetwork.crypto.Signature; + /** * Configuration interface for customizing the initialization and behavior of a Boson DHT node. *

@@ -98,9 +100,9 @@ default int port() { * If {@code null} is returned, the node will generate a random private key upon startup. *

* - * @return the private key as a string, or {@code null} if no key is set. + * @return the private key, or {@code null} if no key is set. */ - default String privateKey() { + default Signature.PrivateKey privateKey() { return null; } @@ -113,7 +115,7 @@ default String privateKey() { * * @return the storage directory path, or {@code null} to disable persistence. */ - default Path dataPath() { + default Path dataDir() { return null; } @@ -122,7 +124,7 @@ default Path dataPath() { * * @return the external storage URL as a string, or {@code null} if not configured. */ - default String storageURL() { + default String storageURI() { return null; } diff --git a/api/src/main/java/io/bosonnetwork/database/Filter.java b/api/src/main/java/io/bosonnetwork/database/Filter.java index 4c00ca6..851a51b 100644 --- a/api/src/main/java/io/bosonnetwork/database/Filter.java +++ b/api/src/main/java/io/bosonnetwork/database/Filter.java @@ -1,3 +1,25 @@ +/* + * 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.database; import java.util.Arrays; @@ -57,7 +79,7 @@ public static Filter eq(String column, Object value) { } /** - * Creates a non-equality filter (column <> #{paramName}). + * Creates a non-equality filter (column <> #{paramName}). * * @param column the database column name * @param paramName the parameter name to bind @@ -72,7 +94,7 @@ public static Filter ne(String column, String paramName, Object value) { } /** - * Creates a non-equality filter (column <> #{paramName}). + * Creates a non-equality filter (column <> #{paramName}). * * @param column the database column name * @param value the value to bind @@ -83,7 +105,7 @@ public static Filter ne(String column, Object value) { } /** - * Creates a less-than filter (column < #{paramName}). + * Creates a less-than filter (column < #{paramName}). * * @param column the database column name * @param paramName the parameter name to bind @@ -98,7 +120,7 @@ public static Filter lt(String column, String paramName, Object value) { } /** - * Creates a less-than filter (column < #{paramName}). + * Creates a less-than filter (column < #{paramName}). * * @param column the database column name * @param value the value to bind @@ -109,7 +131,7 @@ public static Filter lt(String column, Object value) { } /** - * Creates a less-than-or-equal filter (column <= #{paramName}). + * Creates a less-than-or-equal filter (column <= #{paramName}). * * @param column the database column name * @param paramName the parameter name to bind @@ -124,7 +146,7 @@ public static Filter lte(String column, String paramName, Object value) { } /** - * Creates a less-than-or-equal filter (column <= #{paramName}). + * Creates a less-than-or-equal filter (column <= #{paramName}). * * @param column the database column name * @param value the value to bind @@ -135,7 +157,7 @@ public static Filter lte(String column, Object value) { } /** - * Creates a greater-than filter (column > #{paramName}). + * Creates a greater-than filter (column > #{paramName}). * * @param column the database column name * @param paramName the parameter name to bind @@ -150,7 +172,7 @@ public static Filter gt(String column, String paramName, Object value) { } /** - * Creates a greater-than filter (column > #{paramName}). + * Creates a greater-than filter (column > #{paramName}). * * @param column the database column name * @param value the value to bind @@ -161,7 +183,7 @@ public static Filter gt(String column, Object value) { } /** - * Creates a greater-than-or-equal filter (column >= #{paramName}). + * Creates a greater-than-or-equal filter (column >= #{paramName}). * * @param column the database column name * @param paramName the parameter name to bind @@ -176,7 +198,7 @@ public static Filter gte(String column, String paramName, Object value) { } /** - * Creates a greater-than-or-equal filter (column >= #{paramName}). + * Creates a greater-than-or-equal filter (column >= #{paramName}). * * @param column the database column name * @param value the value to bind @@ -303,6 +325,11 @@ public boolean isEmpty() { } + /** + * Returns the parameter bindings for this filter. + * + * @return a map of parameter names to their values + */ public Map getParams() { return Map.of(); } diff --git a/api/src/main/java/io/bosonnetwork/database/Ordering.java b/api/src/main/java/io/bosonnetwork/database/Ordering.java index fc14bb1..9551d35 100644 --- a/api/src/main/java/io/bosonnetwork/database/Ordering.java +++ b/api/src/main/java/io/bosonnetwork/database/Ordering.java @@ -1,3 +1,25 @@ +/* + * 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.database; import java.util.ArrayList; @@ -11,11 +33,11 @@ * Supports multiple fields and prevents SQL injection by validating column names. *

* Example: + *

  * Ordering order = Ordering.by("name").asc()
  *                          .then("created").desc();
- * 
* String sql = order.toSql(); // " ORDER BY name ASC, created DESC" - *

+ *
*/ public class Ordering { /** Special instance representing no ordering. Method toSql() returns an empty string. */ @@ -33,9 +55,11 @@ public enum Direction { /** * Represents a column and its sorting direction in an SQL ORDER BY clause. - * A Field is used to specify the sorting criteria for a query. Each Field contains: - * - A column, representing the name of the database column to sort by. - * - A direction, indicating whether the sorting should be ascending or descending. + *

+ * A Field is used to specify the sorting criteria for a query. Each Field contains + * a column name representing the database column to sort by, and a direction + * indicating whether the sorting should be ascending or descending. + *

* * @param column representing the name of the database column to sort by. * @param direction indicating whether the sorting should be ascending or descending. @@ -43,6 +67,11 @@ public enum Direction { public record Field(String column, Direction direction) { } + /** + * Constructs an Ordering with the specified list of fields. + * + * @param fields the list of fields to order by + */ private Ordering(List fields) { this.fields = Collections.unmodifiableList(fields); } @@ -92,7 +121,7 @@ public String toSql() { * Generates a unique identifier string representing the ordering configuration * based on the fields. If no fields are present, it returns "none". * - * @return a string in the format "orderBy___..." or "none" if no fields are defined + * @return a string in the format "orderBy_column_direction_..." or "none" if no fields are defined */ public String identifier() { if (fields.isEmpty()) @@ -192,6 +221,16 @@ public Ordering build() { } } + /** + * Validates that the column name contains only safe characters. + *

+ * Only letters, digits, and underscores are allowed (safe for SQL identifiers). + * Optionally supports qualified names with a single dot (e.g., "table.column"). + *

+ * + * @param column the column name to validate + * @throws IllegalArgumentException if the column name is invalid + */ private static void validateColumn(String column) { // Only letters, digits, and underscore allowed (safe for SQL identifiers) if (!column.matches("^[A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_][A-Za-z0-9_]*)?$")) diff --git a/api/src/main/java/io/bosonnetwork/database/Pagination.java b/api/src/main/java/io/bosonnetwork/database/Pagination.java index 9ac25da..6490cc0 100644 --- a/api/src/main/java/io/bosonnetwork/database/Pagination.java +++ b/api/src/main/java/io/bosonnetwork/database/Pagination.java @@ -1,15 +1,39 @@ +/* + * 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.database; import java.util.Map; /** * Helper class for building SQL LIMIT/OFFSET clauses safely. - *

+ *

* Example: * Pagination p = Pagination.page(3, 20); // pageIndex=3, pageSize=20 * p.toSql(); // " OFFSET 40 LIMIT 20" + *

*/ public class Pagination { + /** Special instance representing no pagination. Method toSql() returns an empty string. */ public static final Pagination NONE = new Pagination(0, 0); private final long offset; diff --git a/api/src/main/java/io/bosonnetwork/metrics/Measured.java b/api/src/main/java/io/bosonnetwork/metrics/Measured.java index b98c6c5..921ba8d 100644 --- a/api/src/main/java/io/bosonnetwork/metrics/Measured.java +++ b/api/src/main/java/io/bosonnetwork/metrics/Measured.java @@ -1,3 +1,25 @@ +/* + * 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.metrics; /** diff --git a/api/src/main/java/io/bosonnetwork/metrics/Metrics.java b/api/src/main/java/io/bosonnetwork/metrics/Metrics.java index bb40889..b6a6868 100644 --- a/api/src/main/java/io/bosonnetwork/metrics/Metrics.java +++ b/api/src/main/java/io/bosonnetwork/metrics/Metrics.java @@ -1,3 +1,25 @@ +/* + * 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.metrics; /** diff --git a/api/src/main/java/io/bosonnetwork/service/BosonService.java b/api/src/main/java/io/bosonnetwork/service/BosonService.java index ac0e84f..1072ed6 100644 --- a/api/src/main/java/io/bosonnetwork/service/BosonService.java +++ b/api/src/main/java/io/bosonnetwork/service/BosonService.java @@ -25,9 +25,11 @@ import java.util.concurrent.CompletableFuture; +import io.bosonnetwork.Id; + /** * Interface BosonService is the basic abstraction for the extensible service on top of - * Boson super node. This interface describes the basic information about the service + * the Boson super node. This interface describes the basic information about the service * itself and the life-cycle management methods. All super node services should implement * this interface. */ @@ -46,6 +48,45 @@ public interface BosonService { */ String getName(); + /** + * Retrieves the peer identifier associated with the service. + * + * @return an {@link Id} object representing the peer identifier. + */ + Id getPeerId(); + + /** + * Retrieves the host associated with the service. + * + * @return a string representing the host's address. + */ + String getHost(); + + /** + * Retrieves the port number associated with the service. + * + * @return an integer representing the port number. + */ + int getPort(); + + /** + * Retrieves an alternative endpoint for the service. + * + * @return a string representing the alternative endpoint. + */ + default String getAlternativeEndpoint() { + return null; + } + + /** + * Checks whether the federation feature is enabled for the service. + * + * @return true if federation is enabled, false otherwise. + */ + default boolean isFederationEnabled() { + return false; + } + /** * Get the running status * diff --git a/api/src/main/java/io/bosonnetwork/service/ClientAuthenticator.java b/api/src/main/java/io/bosonnetwork/service/ClientAuthenticator.java index d684b4e..ba70987 100644 --- a/api/src/main/java/io/bosonnetwork/service/ClientAuthenticator.java +++ b/api/src/main/java/io/bosonnetwork/service/ClientAuthenticator.java @@ -1,11 +1,125 @@ +/* + * 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.service; +import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import io.bosonnetwork.Id; +/** + * Interface for authenticating clients (users and devices). + *

+ * Implementations of this interface provide mechanisms to verify the identity of users and devices + * using cryptographic signatures and other credentials. + */ public interface ClientAuthenticator { + + /** + * Authenticates a user based on their ID and a cryptographic signature. + * + * @param userId the unique identifier of the user + * @param nonce the random challenge data (nonce) used for authentication + * @param signature the digital signature of the nonce, generated using the user's private key + * @return a {@link CompletableFuture} that completes with {@code true} if the user is successfully authenticated, + * or {@code false} otherwise + */ CompletableFuture authenticateUser(Id userId, byte[] nonce, byte[] signature); + /** + * Authenticates a specific device belonging to a user. + * + * @param userId the unique identifier of the user who owns the device + * @param deviceId the unique identifier of the device attempting to authenticate + * @param nonce the random challenge data (nonce) used for authentication + * @param signature the digital signature of the nonce, generated using the device's private key + * @param address the network address (e.g., IP address) from which the device is connecting + * @return a {@link CompletableFuture} that completes with {@code true} if the device is successfully authenticated, + * or {@code false} otherwise + */ CompletableFuture authenticateDevice(Id userId, Id deviceId, byte[] nonce, byte[] signature, String address); + + /** + * Returns a `ClientAuthenticator` instance that allows all authentication attempts. + * The returned authenticator verifies the provided signature against the corresponding + * signature key derived from the user or device ID, enabling universal authentication + * acceptance when the signature is valid. + * + * @return a `ClientAuthenticator` instance that performs signature validation for user + * and device authentication and always allows access if the signature is valid + */ + static ClientAuthenticator allowAll() { + return new ClientAuthenticator() { + @Override + public CompletableFuture authenticateUser(Id userId, byte[] nonce, byte[] signature) { + return userId.toSignatureKey().verify(nonce, signature) ? + CompletableFuture.completedFuture(true) : + CompletableFuture.completedFuture(false); + } + + @Override + public CompletableFuture authenticateDevice(Id userId, Id deviceId, byte[] nonce, byte[] signature, String address) { + return deviceId.toSignatureKey().verify(nonce, signature) ? + CompletableFuture.completedFuture(true) : + CompletableFuture.completedFuture(false); + } + }; + } + + /** + * Creates a `ClientAuthenticator` instance that validates authentication + * attempts based on a provided mapping of users and their associated devices. + * The returned authenticator verifies the provided signature and ensures + * that the user ID and device ID exist in the given map, granting access + * if all checks are satisfied. + * + * @param userDeviceMap a mapping where the keys represent user IDs and the values + * are lists of device IDs associated with each user + * @return a `ClientAuthenticator` instance that performs authentication based + * on the provided user-device mapping and cryptographic signature verification + */ + static ClientAuthenticator allow(Map> userDeviceMap) { + return new ClientAuthenticator() { + @Override + public CompletableFuture authenticateUser(Id userId, byte[] nonce, byte[] signature) { + if (!userDeviceMap.containsKey(userId)) + return CompletableFuture.completedFuture(false); + + return userId.toSignatureKey().verify(nonce, signature) ? + CompletableFuture.completedFuture(true) : + CompletableFuture.completedFuture(false); + } + + @Override + public CompletableFuture authenticateDevice(Id userId, Id deviceId, byte[] nonce, byte[] signature, String address) { + if (!userDeviceMap.containsKey(userId) || !userDeviceMap.get(userId).contains(deviceId)) + return CompletableFuture.completedFuture(false); + + return deviceId.toSignatureKey().verify(nonce, signature) ? + CompletableFuture.completedFuture(true) : + CompletableFuture.completedFuture(false); + } + }; + } } \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/service/ClientAuthorizer.java b/api/src/main/java/io/bosonnetwork/service/ClientAuthorizer.java new file mode 100644 index 0000000..5603a42 --- /dev/null +++ b/api/src/main/java/io/bosonnetwork/service/ClientAuthorizer.java @@ -0,0 +1,60 @@ +/* + * 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.service; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import io.bosonnetwork.Id; + +/** + * Interface for authorizing client requests to access specific services. + *

+ * Implementations of this interface define the logic to determine if a user on a specific device + * is granted permission to use the requested service. + */ +public interface ClientAuthorizer { + /** + * Authorizes the specified user and device for the given service. + * + * @param userId the unique identifier of the user requesting access + * @param deviceId the unique identifier of the device used for the request + * @param serviceId the identifier of the service to be accessed + * @return a {@link CompletableFuture} that completes with a map containing authorization details + * (e.g., tokens, permissions) if successful, or completes exceptionally if authorization fails + */ + CompletableFuture> authorize(Id userId, Id deviceId, String serviceId); + + /** + * Returns a no-operation implementation of {@link ClientAuthorizer}. + * + * This implementation provides an authorization mechanism that always completes + * successfully without performing any checks or validations. It returns an + * empty map to indicate no authorization details are provided. + * + * @return a no-op {@link ClientAuthorizer} instance + */ + static ClientAuthorizer noop() { + return (userId, deviceId, serviceId) -> CompletableFuture.completedFuture(Map.of()); + } +} \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/service/ClientDevice.java b/api/src/main/java/io/bosonnetwork/service/ClientDevice.java index 2ee25c9..b2062fb 100644 --- a/api/src/main/java/io/bosonnetwork/service/ClientDevice.java +++ b/api/src/main/java/io/bosonnetwork/service/ClientDevice.java @@ -1,21 +1,89 @@ +/* + * 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.service; import io.bosonnetwork.Id; +/** + * Interface representing a client device in the Boson network. + *

+ * A client device is associated with a user and can have specific attributes like a name, application information, + * and usage statistics such as creation time, update time, and last seen details. + */ public interface ClientDevice { + /** + * Gets the unique identifier of the device. + * + * @return the device {@link Id} + */ Id getId(); + /** + * Gets the unique identifier of the user who owns this device. + * + * @return the user {@link Id} + */ Id getUserId(); + /** + * Gets the name of the device. + * + * @return the device name + */ String getName(); + /** + * Gets the name of the application associated with this device. + * + * @return the application name + */ String getApp(); + /** + * Gets the timestamp when the device was created. + * + * @return the creation timestamp in milliseconds + */ long getCreated(); + /** + * Gets the timestamp when the device information was last updated. + * + * @return the last update timestamp in milliseconds + */ long getUpdated(); + /** + * Gets the timestamp when the device was last seen active. + * + * @return the last-seen timestamp in milliseconds + */ long getLastSeen(); + /** + * Gets the last known network address (e.g., IP address) of the device. + * + * @return the last known address as a String + */ String getLastAddress(); } \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/service/ClientUser.java b/api/src/main/java/io/bosonnetwork/service/ClientUser.java index c133330..50067b1 100644 --- a/api/src/main/java/io/bosonnetwork/service/ClientUser.java +++ b/api/src/main/java/io/bosonnetwork/service/ClientUser.java @@ -1,27 +1,111 @@ +/* + * 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.service; import io.bosonnetwork.Id; +/** + * Interface representing a registered user on the Boson super node. + *

+ * This interface provides access to user profile information such as identification, + * display name, contact details, and account status. + */ public interface ClientUser { + /** + * Gets the unique identifier of the user. + * + * @return the user {@link Id} + */ Id getId(); + /** + * Verifies if the provided passphrase matches the user's passphrase. + * + * @param passphrase the passphrase to verify + * @return {@code true} if the passphrase is valid, {@code false} otherwise + */ boolean verifyPassphrase(String passphrase); + /** + * Gets the display name of the user. + * + * @return the user's name + */ String getName(); + /** + * Gets the avatar identifier or URL of the user. + * + * @return the avatar string + */ String getAvatar(); + /** + * Gets the email address associated with the user. + * + * @return the user's email address + */ String getEmail(); + /** + * Gets the biography or description of the user. + * + * @return the user's bio + */ String getBio(); + /** + * Gets the timestamp when the user account was created. + * + * @return the creation timestamp in milliseconds + */ long getCreated(); + /** + * Gets the timestamp when the user profile was last updated. + * + * @return the last update timestamp in milliseconds + */ long getUpdated(); + /** + * Checks if the user presence is currently announced to the network. + * + * @return {@code true} if the user is announced, {@code false} otherwise + */ boolean isAnnounce(); + /** + * Gets the timestamp of the last announcement of the user. + * + * @return the last announcement timestamp in milliseconds + */ long getLastAnnounced(); + /** + * Gets the name of the subscription plan associated with the user. + * + * @return the plan name + */ String getPlanName(); } \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/service/Clients.java b/api/src/main/java/io/bosonnetwork/service/Clients.java index eebecda..1bd3c19 100644 --- a/api/src/main/java/io/bosonnetwork/service/Clients.java +++ b/api/src/main/java/io/bosonnetwork/service/Clients.java @@ -1,3 +1,25 @@ +/* + * 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.service; import java.util.List; @@ -5,18 +27,65 @@ import io.bosonnetwork.Id; +/** + * Client manager interface (Read-only) for the Boson Super Node services. + *

+ * This interface provides methods to query information about client users and their associated devices + * registered within the network. It allows retrieving user details, checking for user existence, + * and listing or verifying devices associated with users. + */ public interface Clients { + /** + * Retrieves the user information for a given user ID. + * + * @param userId the unique identifier of the user to retrieve + * @return a {@link CompletableFuture} that completes with the {@link ClientUser} object if found, + * or completes with {@code null} if the user does not exist + */ CompletableFuture getUser(Id userId); + /** + * Checks if a user with the specified ID exists. + * + * @param userId the unique identifier of the user to check + * @return a {@link CompletableFuture} that completes with {@code true} if the user exists, + * or {@code false} otherwise + */ CompletableFuture existsUser(Id userId); + /** + * Retrieves a list of all devices associated with a specific user. + * + * @param userId the unique identifier of the user whose devices are to be retrieved + * @return a {@link CompletableFuture} that completes with a list of {@link ClientDevice} objects belonging to the user + */ CompletableFuture> getDevices(Id userId); + /** + * Retrieves the device information for a given device ID. + * + * @param deviceId the unique identifier of the device to retrieve + * @return a {@link CompletableFuture} that completes with the {@link ClientDevice} object if found, + * or completes with {@code null} if the device does not exist + */ CompletableFuture getDevice(Id deviceId); + /** + * Checks if a device with the specified ID exists. + * + * @param deviceId the unique identifier of the device to check + * @return a {@link CompletableFuture} that completes with {@code true} if the device exists, + * or {@code false} otherwise + */ CompletableFuture existsDevice(Id deviceId); + /** + * Checks if a specific device exists and is associated with the specified user. + * + * @param userId the unique identifier of the user + * @param deviceId the unique identifier of the device + * @return a {@link CompletableFuture} that completes with {@code true} if the device exists and belongs to the user, + * or {@code false} otherwise + */ CompletableFuture existsDevice(Id userId, Id deviceId); - - // CompletableFuture addDevice(Id deviceId, Id userId, String name, String app); } \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/service/DefaultServiceContext.java b/api/src/main/java/io/bosonnetwork/service/DefaultServiceContext.java index e9e384a..22f48b8 100644 --- a/api/src/main/java/io/bosonnetwork/service/DefaultServiceContext.java +++ b/api/src/main/java/io/bosonnetwork/service/DefaultServiceContext.java @@ -1,3 +1,26 @@ +/* + * Copyright (c) 2022 - 2023 trinity-tech.io + * 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.service; import java.nio.file.Path; @@ -8,76 +31,140 @@ import io.bosonnetwork.Id; import io.bosonnetwork.Node; -import io.bosonnetwork.access.AccessManager; /** - * Default {@link ServiceContext} implementation. + * Default implementation of the {@link ServiceContext} interface. + *

+ * This class provides a standard implementation for accessing service context information, + * including the Vert.x instance, the Boson node, authentication/authorization components, + * federation details, configuration, and data persistence paths. */ public class DefaultServiceContext implements ServiceContext { private final Vertx vertx; private final Node node; - private final AccessManager accessManager; + private final ClientAuthenticator clientAuthenticator; + private final ClientAuthorizer clientAuthorizer; + private final FederationAuthenticator federationAuthenticator; + private final Federation federation; private final Map configuration; - private final Map properties; - private final Path dataPath; + private final Path dataDir; + private Map properties; /** * Creates a new {@link ServiceContext} instance. * - * @param vertx the Vert.x instance. - * @param node the host Boson node. - * @param accessManager the {@link io.bosonnetwork.access.AccessManager} instance that - * provided by the host node. - * @param configuration the configuration data of the service. - * @param dataPath the persistence data path, or null if not available. + * @param vertx the Vert.x instance to be used + * @param node the host Boson node + * @param clientAuthenticator the authenticator for client connections + * @param clientAuthorizer the authorizer for client requests + * @param federationAuthenticator the authenticator for federation communications + * @param federation the federation instance + * @param configuration the configuration data for the service + * @param dataDir the path to the persistence data directory, or {@code null} if not available */ - public DefaultServiceContext(Vertx vertx, Node node, AccessManager accessManager, - Map configuration, Path dataPath) { + public DefaultServiceContext(Vertx vertx, Node node, ClientAuthenticator clientAuthenticator, ClientAuthorizer clientAuthorizer, + FederationAuthenticator federationAuthenticator, Federation federation, + Map configuration, Path dataDir) { this.vertx = vertx; this.node = node; - this.accessManager = accessManager != null ? accessManager : AccessManager.getDefault(); + this.clientAuthenticator = clientAuthenticator; + this.clientAuthorizer = clientAuthorizer; + this.federationAuthenticator = federationAuthenticator; + this.federation = federation; this.configuration = configuration; - this.dataPath = dataPath; - this.properties = new HashMap<>(); + this.dataDir = dataDir; } + /** + * {@inheritDoc} + */ @Override public Vertx getVertx() { return vertx; } + /** + * {@inheritDoc} + */ @Override public Node getNode() { return node; } + /** + * {@inheritDoc} + */ @Override public Id getNodeId() { return node.getId(); } + /** + * {@inheritDoc} + */ @Override - public Path getDataPath() { - return dataPath; + public Path getDataDir() { + return dataDir; } + /** + * {@inheritDoc} + */ @Override - public AccessManager getAccessManager() { - return accessManager; + public ClientAuthenticator getClientAuthenticator() { + return clientAuthenticator; } + /** + * {@inheritDoc} + */ + @Override + public ClientAuthorizer getClientAuthorizer() { + return clientAuthorizer; + } + + /** + * {@inheritDoc} + */ + @Override + public FederationAuthenticator getFederationAuthenticator() { + return federationAuthenticator; + } + + /** + * {@inheritDoc} + */ + @Override + public Federation getFederation() { + return federation; + } + + /** + * {@inheritDoc} + */ @Override public Map getConfiguration() { return configuration; } + private Map properties() { + return properties == null ? properties = new HashMap<>() : properties; + } + + /** + * {@inheritDoc} + */ @Override - public Object setProperty(String name, Object value) { - return properties.put(name, value); + public Object setProperty(Object key, Object value) { + return properties().put(key, value); } + /** + * {@inheritDoc} + */ @Override - public Object getProperty(String name) { - return properties.get(name); + @SuppressWarnings("unchecked") + public T getProperty(Object key) { + return (T) properties().get(key); } } \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/service/FederatedNode.java b/api/src/main/java/io/bosonnetwork/service/FederatedNode.java index c3de0a3..071f4ae 100644 --- a/api/src/main/java/io/bosonnetwork/service/FederatedNode.java +++ b/api/src/main/java/io/bosonnetwork/service/FederatedNode.java @@ -1,35 +1,138 @@ +/* + * 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.service; import io.bosonnetwork.Id; +/** + * Interface representing a super node within the boson federation. + *

+ * This interface provides access to the properties of a federated node, including its identity, + * connection details, software information, and metadata such as trust status and reputation. + */ public interface FederatedNode { + /** + * Gets the unique identifier of the federated node. + * + * @return the node {@link Id} + */ Id getId(); + /** + * Gets the hostname or IP address of the node. + * + * @return the host string + */ String getHost(); + /** + * Gets the port number on which the node accepts connections. + * + * @return the port number + */ int getPort(); + /** + * Gets the API endpoint URL for the node. + * + * @return the API endpoint string + */ String getApiEndpoint(); + /** + * Gets the name of the software running on the node. + * + * @return the software name + */ String getSoftware(); + /** + * Gets the version of the software running on the node. + * + * @return the software version + */ String getVersion(); + /** + * Gets the display name of the node. + * + * @return the node name + */ String getName(); + /** + * Gets the URL or identifier for the node's logo. + * + * @return the logo string + */ String getLogo(); + /** + * Gets the website URL associated with the node. + * + * @return the website URL + */ String getWebsite(); + /** + * Gets the contact information for the node administrator. + * + * @return the contact string + */ String getContact(); + /** + * Gets the description of the node. + * + * @return the node description + */ String getDescription(); + /** + * Checks if the node is considered trusted within the federation. + * + * @return {@code true} if the node is trusted, {@code false} otherwise + */ boolean isTrusted(); + /** + * Gets the reputation score of the node. + * + * @return the reputation score as an integer + */ int getReputation(); + /** + * Gets the timestamp when the node was added to the federation. + * + * @return the creation timestamp in milliseconds + */ long getCreated(); + /** + * Gets the timestamp when the node information was last updated. + * + * @return the last update timestamp in milliseconds + */ long getUpdated(); } \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/service/FederatedService.java b/api/src/main/java/io/bosonnetwork/service/FederatedService.java deleted file mode 100644 index 5772b50..0000000 --- a/api/src/main/java/io/bosonnetwork/service/FederatedService.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.bosonnetwork.service; - -import io.bosonnetwork.Id; - -public interface FederatedService { - Id getPeerId(); - - Id getNodeId(); - - Id getOriginId(); - - String getHost(); - - int getPort(); - - String getAlternativeEndpoint(); - - String getServiceId(); - - String getServiceName(); - - String getEndpoint(); - - long getCreated(); - - long getUpdated(); -} \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/service/Federation.java b/api/src/main/java/io/bosonnetwork/service/Federation.java index 95111e0..8cda9e1 100644 --- a/api/src/main/java/io/bosonnetwork/service/Federation.java +++ b/api/src/main/java/io/bosonnetwork/service/Federation.java @@ -1,27 +1,78 @@ +/* + * 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.service; import java.util.concurrent.CompletableFuture; import io.bosonnetwork.Id; +/** + * Federation manager (read-only) interface for the Boson Super Node services. + *

+ * This interface provides methods to interact with and query information about other nodes + * within the federation, including retrieving node details, checking existence, and finding + * specific services hosted by federated nodes. + */ public interface Federation { + /** + * Retrieves a federated node by its ID. + * + * @param nodeId the unique identifier of the node to retrieve + * @param federateIfNotExists if {@code true}, attempts to add the node to the federation if it is not already known + * @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 getNode(Id nodeId, boolean federateIfNotExists); + /** + * Retrieves a federated node by its ID. + *

+ * This is a convenience method that calls {@link #getNode(Id, boolean)} with {@code federateIfNotExists} set to {@code false}. + * + * @param nodeId the unique identifier of the node to retrieve + * @return a {@link CompletableFuture} that completes with the {@link FederatedNode} object if found, + * or completes exceptionally/with null if the node is not part of the federation + */ default CompletableFuture getNode(Id nodeId) { return getNode(nodeId, false); } + /** + * Checks if a node with the specified ID exists in the federation. + * + * @param nodeId the unique identifier of the node to check + * @return a {@link CompletableFuture} that completes with {@code true} if the node exists, + * or {@code false} otherwise + */ public CompletableFuture existsNode(Id nodeId); - - - // public CompletableFuture addNode(FederatedNode node); - - // public CompletableFuture updateNode(FederatedNode node); - - // public CompletableFuture removeNode(Id nodeId); - - // public CompletableFuture> getAllServices(Id nodeId); - - public CompletableFuture getService(Id nodeId, Id peerId); + /** + * Retrieves information about a specific service hosted by a federated node. + * + * @param nodeId the unique identifier of the node hosting the service + * @param peerId the unique identifier of the service peer + * @return a {@link CompletableFuture} that completes with the {@link ServiceInfo} object if found, + * or completes exceptionally/with null if the service cannot be located + */ + public CompletableFuture getService(Id nodeId, Id peerId); } \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/service/FederationAuthenticator.java b/api/src/main/java/io/bosonnetwork/service/FederationAuthenticator.java index f017fad..5a8d5a2 100644 --- a/api/src/main/java/io/bosonnetwork/service/FederationAuthenticator.java +++ b/api/src/main/java/io/bosonnetwork/service/FederationAuthenticator.java @@ -1,11 +1,146 @@ +/* + * 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.service; +import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import io.bosonnetwork.Id; +/** + * Interface for authenticating nodes and peers within a federation. + *

+ * Implementations of this interface provide mechanisms to verify the identity of other nodes + * in the federation using cryptographic challenges and signatures. + */ public interface FederationAuthenticator { + + /** + * Authenticates a node in the federation. + * + * @param nodeId the unique identifier of the node to be authenticated + * @param nonce the random challenge data (nonce) used for authentication + * @param signature the digital signature of the nonce, generated using the node's private key + * @return a {@link CompletableFuture} that completes with {@code true} if the node is successfully authenticated, + * or {@code false} otherwise + */ CompletableFuture authenticateNode(Id nodeId, byte[] nonce, byte[] signature); + /** + * Authenticates a peer associated with a node in the federation. + * + * @param nodeId the unique identifier of the node managing the peer + * @param peerId the unique identifier of the peer to be authenticated + * @param nonce the random challenge data (nonce) used for authentication + * @param signature the digital signature of the nonce, generated using the peer's private key + * @return a {@link CompletableFuture} that completes with {@code true} if the peer is successfully authenticated, + * or {@code false} otherwise + */ CompletableFuture authenticatePeer(Id nodeId, Id peerId, byte[] nonce, byte[] signature); + + /** + * Provides a FederationAuthenticator implementation that allows all authentication attempts. + * The returned authenticator verifies the provided signature against the corresponding + * signature key derived from the node or peer ID, enabling universal authentication + * acceptance when the signature is valid. + * + * @return a FederationAuthenticator instance that performs authentication by signature verification + */ + static FederationAuthenticator allowAll() { + return new FederationAuthenticator() { + @Override + public CompletableFuture authenticateNode(Id nodeId, byte[] nonce, byte[] signature) { + Objects.requireNonNull(nodeId, "nodeId"); + Objects.requireNonNull(nonce, "nonce"); + Objects.requireNonNull(signature, "signature"); + + return nodeId.toSignatureKey().verify(nonce, signature) ? + CompletableFuture.completedFuture(true) : + CompletableFuture.completedFuture(false); + } + + @Override + public CompletableFuture authenticatePeer(Id nodeId, Id peerId, byte[] nonce, byte[] signature) { + Objects.requireNonNull(nodeId, "nodeId"); + Objects.requireNonNull(peerId, "peerId"); + Objects.requireNonNull(nonce, "nonce"); + Objects.requireNonNull(signature, "signature"); + + return peerId.toSignatureKey().verify(nonce, signature) ? + CompletableFuture.completedFuture(true) : + CompletableFuture.completedFuture(false); + } + }; + } + + /** + * Provides a FederationAuthenticator implementation that restricts successful + * authentication based on the supplied nodeServicesMap. The returned + * FederationAuthenticator validates that the provided node or peer ID is + * included in the nodeServicesMap and verifies the associated digital signature. + * + * @param nodeServicesMap a map where each key is a node ID, and each value is + * a list of peer IDs associated with that node. This + * map is used to determine whether a given node or + * peer is authorized for authentication. + * @return a FederationAuthenticator instance that authenticates nodes and peers + * according to the provided nodeServicesMap and performs signature + * verification. + */ + static FederationAuthenticator allow(Map> nodeServicesMap) { + Objects.requireNonNull(nodeServicesMap, "nodeServicesMap"); + + return new FederationAuthenticator() { + @Override + public CompletableFuture authenticateNode(Id nodeId, byte[] nonce, byte[] signature) { + Objects.requireNonNull(nodeId, "nodeId"); + Objects.requireNonNull(nonce, "nonce"); + Objects.requireNonNull(signature, "signature"); + + if (!nodeServicesMap.containsKey(nodeId)) + return CompletableFuture.completedFuture(false); + + return nodeId.toSignatureKey().verify(nonce, signature) ? + CompletableFuture.completedFuture(true) : + CompletableFuture.completedFuture(false); + } + + @Override + public CompletableFuture authenticatePeer(Id nodeId, Id peerId, byte[] nonce, byte[] signature) { + Objects.requireNonNull(nodeId, "nodeId"); + Objects.requireNonNull(peerId, "peerId"); + Objects.requireNonNull(nonce, "nonce"); + Objects.requireNonNull(signature, "signature"); + + if (!nodeServicesMap.containsKey(nodeId) || !nodeServicesMap.get(nodeId).contains(peerId)) + return CompletableFuture.completedFuture(false); + + return nodeId.toSignatureKey().verify(nonce, signature) ? + CompletableFuture.completedFuture(true) : + CompletableFuture.completedFuture(false); + } + }; + } } \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/service/ServiceContext.java b/api/src/main/java/io/bosonnetwork/service/ServiceContext.java index 7cb569c..55e349b 100644 --- a/api/src/main/java/io/bosonnetwork/service/ServiceContext.java +++ b/api/src/main/java/io/bosonnetwork/service/ServiceContext.java @@ -30,7 +30,6 @@ import io.bosonnetwork.Id; import io.bosonnetwork.Node; -import io.bosonnetwork.access.AccessManager; /** * The ServiceContext interface represents a context in which the services can access the @@ -62,15 +61,35 @@ public interface ServiceContext { * * @return the {@code Path} object. */ - Path getDataPath(); + Path getDataDir(); /** - * Gets the {@link io.bosonnetwork.access.AccessManager} instance that provided by - * the host Boson node. + * Gets the client authenticator instance. * - * @return the AccessManager interface. + * @return the {@link ClientAuthenticator} used for authenticating clients. */ - AccessManager getAccessManager(); + ClientAuthenticator getClientAuthenticator(); + + /** + * Gets the client authorizer instance. + * + * @return the {@link ClientAuthorizer} used for authorizing client requests. + */ + ClientAuthorizer getClientAuthorizer(); + + /** + * Gets the federation authenticator instance. + * + * @return the {@link FederationAuthenticator} used for authenticating federation requests. + */ + FederationAuthenticator getFederationAuthenticator(); + + /** + * Gets the federation instance. + * + * @return the {@link Federation} object. + */ + Federation getFederation(); /** * Gets the service configuration data. @@ -82,19 +101,20 @@ public interface ServiceContext { /** * Set the service runtime property * - * @param name the property name. + * @param key the property key. * @param value the new value to be associated with the property name. - * @return the previous value associated with {@code name}. + * @return the previous value associated with {@code key}. */ - Object setProperty(String name, Object value); + Object setProperty(Object key, Object value); /** - * Returns the value to which the specified name, or {@code null} if the service - * contains no property value for the name. + * Returns the value to which the specified key is mapped, or {@code null} if the service + * contains no property value for the key. * - * @param name the property name. + * @param key the property key. + * @param the type of the property value. * @return the value of the specified property, or * {@code null} if the service contains no mapping for the property. */ - Object getProperty(String name); + T getProperty(Object key); } \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/service/ServiceInfo.java b/api/src/main/java/io/bosonnetwork/service/ServiceInfo.java new file mode 100644 index 0000000..1152028 --- /dev/null +++ b/api/src/main/java/io/bosonnetwork/service/ServiceInfo.java @@ -0,0 +1,89 @@ +/* + * 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.service; + +import io.bosonnetwork.Id; + +/** + * Interface containing information about a super node service instance. + *

+ * This interface encapsulates details about where a service is running (host, port), + * its identity (peer ID, node ID), and identifiers for the service itself. + */ +public interface ServiceInfo { + /** + * Gets the unique peer identifier associated with the service. + * + * @return the peer {@link Id} + */ + Id getPeerId(); + + /** + * Gets the unique identifier of the node hosting the service. + * + * @return the node {@link Id} + */ + Id getNodeId(); + + /** + * Gets the origin identifier, typically representing the entity responsible for the service. + * + * @return the origin {@link Id} + */ + Id getOriginId(); + + /** + * Gets the hostname or IP address where the service is accessible. + * + * @return the host string + */ + String getHost(); + + /** + * Gets the network port number where the service is listening. + * + * @return the port number + */ + int getPort(); + + /** + * Gets an alternative endpoint URL or URI for accessing the service. + * + * @return the alternative endpoint string, or {@code null} if not available + */ + String getAlternativeEndpoint(); + + /** + * Gets the unique identifier string for the specific service type. + * + * @return the service ID + */ + String getServiceId(); + + /** + * Gets the human-readable name of the service. + * + * @return the service name + */ + String getServiceName(); +} \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/utils/AddressUtils.java b/api/src/main/java/io/bosonnetwork/utils/AddressUtils.java index 726bbad..259e261 100644 --- a/api/src/main/java/io/bosonnetwork/utils/AddressUtils.java +++ b/api/src/main/java/io/bosonnetwork/utils/AddressUtils.java @@ -26,6 +26,7 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.net.DatagramSocket; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; @@ -36,7 +37,6 @@ import java.net.StandardProtocolFamily; import java.net.URL; import java.net.UnknownHostException; -import java.nio.channels.DatagramChannel; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -558,7 +558,7 @@ public static InetAddress getDefaultRouteAddress(Class ty InetAddress target = null; ProtocolFamily family = type == Inet6Address.class ? StandardProtocolFamily.INET6 : StandardProtocolFamily.INET; - try (DatagramChannel ch = DatagramChannel.open(family)) { + try (DatagramSocket socket = new DatagramSocket()) { if (type == Inet4Address.class) target = InetAddress.getByAddress(new byte[]{8, 8, 8, 8}); else if (type == Inet6Address.class) @@ -566,9 +566,8 @@ else if (type == Inet6Address.class) else throw new IllegalArgumentException("Unsupported type: " + type); - ch.connect(new InetSocketAddress(target, 63)); - InetSocketAddress soa = (InetSocketAddress) ch.getLocalAddress(); - InetAddress local = soa.getAddress(); + socket.connect(new InetSocketAddress(target, 63)); + InetAddress local = socket.getLocalAddress(); if (type.isInstance(local) && !local.isAnyLocalAddress()) return local; diff --git a/api/src/main/java/io/bosonnetwork/utils/ApplicationLock.java b/api/src/main/java/io/bosonnetwork/utils/ApplicationLock.java index 4aee905..587852c 100644 --- a/api/src/main/java/io/bosonnetwork/utils/ApplicationLock.java +++ b/api/src/main/java/io/bosonnetwork/utils/ApplicationLock.java @@ -1,3 +1,25 @@ +/* + * 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.utils; import java.io.File; diff --git a/api/src/main/java/io/bosonnetwork/utils/ConfigMap.java b/api/src/main/java/io/bosonnetwork/utils/ConfigMap.java new file mode 100644 index 0000000..08e3460 --- /dev/null +++ b/api/src/main/java/io/bosonnetwork/utils/ConfigMap.java @@ -0,0 +1,545 @@ +/* + * 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.utils; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalUnit; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * A wrapper around a {@code Map} that provides type-safe configuration value retrieval. + *

+ * This class offers convenient methods to retrieve configuration values as specific types (String, Number, + * Integer, Long, Boolean, Duration, Port, etc.) with automatic type conversion and validation. + * It also supports default values for optional configuration parameters. + *

+ */ +public class ConfigMap implements Map { + private final Map map; + + /** + * Constructs a new ConfigMap wrapping the provided map. + * + * @param map the underlying map to wrap, must not be null + * @throws NullPointerException if map is null + */ + public ConfigMap(Map map) { + Objects.requireNonNull(map); + this.map = map; + } + + /** + * Retrieves a string value for the specified key. + *

+ * If the value is an Enum, returns its name. Otherwise, converts the value to string using toString(). + *

+ * + * @param key the configuration key, must not be null + * @return the string value + * @throws NullPointerException if key is null + * @throws IllegalArgumentException if the key is missing + */ + public String getString(String key) { + Objects.requireNonNull(key); + Object val = map.get(key); + if (val == null) + throw new IllegalArgumentException("Missing value - " + key); + else if (val instanceof String s) + return s; + else if (val instanceof Enum e) + return e.name(); + else + return val.toString(); + } + + /** + * Retrieves a string value for the specified key, or returns a default value if the key is not present. + * + * @param key the configuration key, must not be null + * @param def the default value to return if the key is not present + * @return the string value or the default value + * @throws NullPointerException if key is null + */ + public String getString(String key, String def) { + Objects.requireNonNull(key); + return map.containsKey(key) ? getString(key) : def; + } + + /** + * Retrieves a numeric value for the specified key. + *

+ * Supports conversion from Boolean (true=1, false=0) and String (parsed as Double). + *

+ * + * @param key the configuration key, must not be null + * @return the numeric value + * @throws NullPointerException if key is null + * @throws IllegalArgumentException if the key is missing or the value cannot be converted to a number + */ + public Number getNumber(String key) { + Objects.requireNonNull(key); + Object val = map.get(key); + if (val == null) + throw new IllegalArgumentException("Missing value - " + key); + else if (val instanceof Number n) + return n; + else if (val instanceof Boolean b) + return b ? 1 : 0; + else if (val instanceof String s) + try { + return Double.parseDouble(s); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid number value - " + key + ": " + val); + } + else + throw new IllegalArgumentException("Invalid number value - " + key + ": " + val); + } + + /** + * Retrieves a numeric value for the specified key, or returns a default value if the key is not present. + * + * @param key the configuration key, must not be null + * @param def the default value to return if the key is not present + * @return the numeric value or the default value + * @throws NullPointerException if key is null + */ + public Number getNumber(String key, Number def) { + Objects.requireNonNull(key); + return map.containsKey(key) ? getNumber(key) : def; + } + + /** + * Retrieves an integer value for the specified key. + *

+ * Supports conversion from Number (using intValue()), Boolean (true=1, false=0), and String (parsed as Integer). + *

+ * + * @param key the configuration key, must not be null + * @return the integer value + * @throws NullPointerException if key is null + * @throws IllegalArgumentException if the key is missing or the value cannot be converted to an integer + */ + public int getInteger(String key) { + Objects.requireNonNull(key); + Object val = map.get(key); + if (val == null) + throw new IllegalArgumentException("Missing value - " + key); + else if (val instanceof Integer i) + return i; // Avoids unnecessary unbox/box + else if (val instanceof Number n) + return n.intValue(); + else if (val instanceof Boolean b) + return b ? 1 : 0; + else if (val instanceof String s) + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid integer value - " + key + ": " + val); + } + else + throw new IllegalArgumentException("Invalid integer value - " + key + ": " + val); + } + + /** + * Retrieves an integer value for the specified key, or returns a default value if the key is not present. + * + * @param key the configuration key, must not be null + * @param def the default value to return if the key is not present + * @return the integer value or the default value + * @throws NullPointerException if key is null + */ + public int getInteger(String key, int def) { + Objects.requireNonNull(key); + return map.containsKey(key) ? getInteger(key) : def; + } + + /** + * Retrieves a long value for the specified key. + *

+ * Supports conversion from Number (using longValue()), Boolean (true=1L, false=0L), and String (parsed as Long). + *

+ * + * @param key the configuration key, must not be null + * @return the long value + * @throws NullPointerException if key is null + * @throws IllegalArgumentException if the key is missing or the value cannot be converted to a long + */ + public long getLong(String key) { + Objects.requireNonNull(key); + Object val = map.get(key); + if (val == null) + throw new IllegalArgumentException("Missing value - " + key); + else if (val instanceof Long l) + return l; // Avoids unnecessary unbox/box + else if (val instanceof Number n) + return n.longValue(); + else if (val instanceof Boolean b) + return b ? 1L : 0L; + else if (val instanceof String s) + try { + return Long.parseLong(s); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid long value - " + key + ": " + val); + } + else + throw new IllegalArgumentException("Invalid long value - " + key + ": " + val); + } + + /** + * Retrieves a long value for the specified key, or returns a default value if the key is not present. + * + * @param key the configuration key, must not be null + * @param def the default value to return if the key is not present + * @return the long value or the default value + * @throws NullPointerException if key is null + */ + public long getLong(String key, long def) { + Objects.requireNonNull(key); + return map.containsKey(key) ? getLong(key) : def; + } + + /** + * Retrieves a boolean value for the specified key. + *

+ * Supports conversion from String ("true"/"false", case-insensitive) and Integer (0=false, 1=true). + *

+ * + * @param key the configuration key, must not be null + * @return the boolean value + * @throws NullPointerException if key is null + * @throws IllegalArgumentException if the key is missing or the value cannot be converted to a boolean + */ + public boolean getBoolean(String key) { + Objects.requireNonNull(key); + Object val = map.get(key); + if (val == null) + throw new IllegalArgumentException("Missing value - " + key); + else if (val instanceof Boolean b) + return b; + else if (val instanceof String s) + return switch (s.toLowerCase()) { + case "true" -> true; + case "false" -> false; + default -> throw new IllegalArgumentException("Invalid boolean value - " + key + ": " + val); + }; + else if (val instanceof Integer i) + return switch (i) { + case 0 -> false; + case 1 -> true; + default -> throw new IllegalArgumentException("Invalid boolean value - " + key + ": " + val); + }; + else + throw new IllegalArgumentException("Invalid boolean value - " + key + ": " + val); + } + + /** + * Retrieves a boolean value for the specified key, or returns a default value if the key is not present. + * + * @param key the configuration key, must not be null + * @param def the default value to return if the key is not present + * @return the boolean value or the default value + * @throws NullPointerException if key is null + */ + public boolean getBoolean(String key, boolean def) { + Objects.requireNonNull(key); + return map.containsKey(key) ? getBoolean(key) : def; + } + + /** + * Retrieves a duration value for the specified key. + *

+ * The value can be a long/integer (interpreted as milliseconds) or a human-friendly duration string. + * + *

+ * Format: <number><unit> + *
+ * Supported units (case-sensitive): + *

    + *
  • s - seconds
  • + *
  • m - minutes
  • + *
  • h - hours
  • + *
  • d - days
  • + *
  • w - weeks
  • + *
  • M - months
  • + *
  • y - years
  • + *
+ * + * @param key the configuration key, must not be null + * @return the parsed {@code Duration} object + * @throws NullPointerException if key is null + * @throws IllegalArgumentException if the key is missing or the value cannot be parsed as a duration + */ + public Duration getDuration(String key) { + Objects.requireNonNull(key); + Object val = map.get(key); + if (val == null) { + throw new IllegalArgumentException("Missing value - " + key); + } else if (val instanceof Integer i) { + return Duration.ofMillis(i); + } else if (val instanceof Long l) { + return Duration.ofMillis(l); + } else if (val instanceof String s) { + int idx = s.length() - 1; + final char specifier = s.charAt(idx); + final TemporalUnit unit = switch (specifier) { + case 's' -> ChronoUnit.SECONDS; + case 'm' -> ChronoUnit.MINUTES; + case 'h' -> ChronoUnit.HOURS; + case 'd' -> ChronoUnit.DAYS; + case 'w' -> ChronoUnit.WEEKS; + case 'M' -> ChronoUnit.MONTHS; + case 'y' -> ChronoUnit.YEARS; + default -> throw new IllegalArgumentException("Invalid duration value - " + key + ": " + s + + ", units: s, m, h, d, w, M, y"); + }; + + try { + long number = Long.parseLong(s, 0, idx, 10); + return Duration.ofMillis(number * unit.getDuration().toMillis()); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid duration value - " + key + ": " + s, e); + } + } else { + throw new IllegalArgumentException("Invalid duration value - " + key + ": " + val); + } + } + + /** + * Retrieves a duration value for the specified key, or returns a default value if the key is not present. + * + * @param key the configuration key, must not be null + * @param def the default value to return if the key is not present + * @return the duration value or the default value + * @throws NullPointerException if key is null + */ + public Duration getDuration(String key, Duration def) { + Objects.requireNonNull(key); + return map.containsKey(key) ? getDuration(key) : def; + } + + /** + * Retrieves a valid port number for the specified key. + *

+ * The port must be in the valid range [0, 65535]. Supports conversion from Integer and String. + * + * @param key the configuration key, must not be null + * @return the port number + * @throws NullPointerException if key is null + * @throws IllegalArgumentException if the key is missing, the value cannot be converted to an integer, + * or the port is outside the valid range [0, 65535] + */ + public int getPort(String key) { + Objects.requireNonNull(key); + int port; + Object val = map.get(key); + if (val == null) + throw new IllegalArgumentException("Missing port number - " + key); + else if (val instanceof Integer i) + port = i; + else if (val instanceof String s) + try { + port = Integer.parseInt(s); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid port number - " + key + ": " + val); + } + else + throw new IllegalArgumentException("Invalid port number - " + key + ": " + val); + + if (port < 0 || port > 65535) + throw new IllegalArgumentException("Invalid port number - " + key + ": " + port); + return port; + } + + + /** + * Retrieves a valid port number for the specified key, or returns a default value if the key is not present. + *

+ * The port (including the default) must be in the valid range [0, 65535]. + *

+ * + * @param key the configuration key, must not be null + * @param def the default port number to return if the key is not present + * @return the port number or the default value + * @throws NullPointerException if key is null + * @throws IllegalArgumentException if the port is outside the valid range [0, 65535] + */ + public int getPort(String key, int def) { + Objects.requireNonNull(key); + int port = map.containsKey(key) ? getPort(key) : def; + if (port < 0 || port > 65535) + throw new IllegalArgumentException("Invalid port number - " + key + ": " + port); + return port; + } + + /** + * Retrieves a nested configuration object for the specified key. + *

+ * The value must be a Map, which will be wrapped in a new ConfigMap instance. + *

+ * + * @param key the configuration key, must not be null + * @return a ConfigMap wrapping the nested configuration, or null if the key is not present + * @throws NullPointerException if key is null + * @throws IllegalArgumentException if the value is not a Map + */ + public ConfigMap getObject(String key) { + Objects.requireNonNull(key); + Object val = map.get(key); + if (val == null) { + return null; + } else if (val instanceof Map m) { + @SuppressWarnings("unchecked") + Map subMap = (Map) m; + return new ConfigMap(subMap); + } else { + throw new IllegalArgumentException("Invalid object value - " + key + ": " + val); + } + } + + /** + * Retrieves a list value for the specified key. + *

+ * The value must be a List. The returned list is cast to the specified type parameter. + *

+ * + * @param the type of elements in the list + * @param key the configuration key, must not be null + * @return the list value, or null if the key is not present + * @throws NullPointerException if key is null + * @throws IllegalArgumentException if the value is not a List + */ + public List getList(String key) { + Objects.requireNonNull(key); + Object val = map.get(key); + if (val == null) { + return null; + } else if (val instanceof List l) { + @SuppressWarnings("unchecked") + List lst = (List) l; + return lst; + } else { + throw new IllegalArgumentException("Invalid object value - " + key + ": " + val); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int size() { + return map.size(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean containsKey(Object key) { + return map.containsKey(key); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean containsValue(Object value) { + return map.containsValue(value); + } + + /** + * {@inheritDoc} + */ + @Override + public Object get(Object key) { + return map.get(key); + } + + /** + * {@inheritDoc} + */ + @Override + public Object put(String key, Object value) { + return map.put(key, value); + } + + /** + * {@inheritDoc} + */ + @Override + public Object remove(Object key) { + return map.remove(key); + } + + /** + * {@inheritDoc} + */ + @Override + public void putAll(Map m) { + map.putAll(m); + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() { + map.clear(); + } + + /** + * {@inheritDoc} + */ + @Override + public Set keySet() { + return map.keySet(); + } + + /** + * {@inheritDoc} + */ + @Override + public Collection values() { + return map.values(); + } + + /** + * {@inheritDoc} + */ + @Override + public Set> entrySet() { + return map.entrySet(); + } +} \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/utils/FileUtils.java b/api/src/main/java/io/bosonnetwork/utils/FileUtils.java index 16ca8fa..d3f0c45 100644 --- a/api/src/main/java/io/bosonnetwork/utils/FileUtils.java +++ b/api/src/main/java/io/bosonnetwork/utils/FileUtils.java @@ -1,5 +1,4 @@ /* - * Copyright (c) 2022 - 2023 trinity-tech.io * Copyright (c) 2023 - bosonnetwork.io * * Permission is hereby granted, free of charge, to any person obtaining a copy @@ -31,15 +30,28 @@ import java.nio.file.attribute.BasicFileAttributes; /** - * Utility class for common file operations. - * Provides methods to perform tasks such as deleting files. + * Utility class for common file and directory operations. + *

+ * This class provides static utility methods for: + *

    + *
  • File and directory deletion (including recursive deletion)
  • + *
  • Path normalization and resolution
  • + *
  • Platform-specific configuration and data directory retrieval
  • + *
+ * + *

+ * All methods in this class are static and the class cannot be instantiated. */ public class FileUtils { /** - * Deletes the specified file from the file system. + * Deletes the specified file or directory from the file system. + *

+ * If the path points to a directory, this method recursively deletes all contents + * (files and subdirectories) before deleting the directory itself. If the path does + * not exist, this method returns without throwing an exception. * - * @param file the {@link Path} of the file to delete - * @throws IOException if an I/O error occurs while deleting the file + * @param file the {@link Path} of the file or directory to delete, must not be null + * @throws IOException if an I/O error occurs while deleting the file or directory */ public static void deleteFile(Path file) throws IOException { if (Files.notExists(file)) @@ -59,4 +71,178 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOE } }); } + + /** + * Normalizes and resolves a file system path. + *

+ * This method performs the following operations: + *

    + *
  • Normalizes the path (removes redundant elements like "." and "..")
  • + *
  • Returns absolute paths unchanged (after normalization)
  • + *
  • Expands tilde (~) prefix to the user's home directory
  • + *
  • Returns relative paths unchanged (after normalization)
  • + *
+ * + * @param path the path to normalize, may be null + * @return the normalized path, or null if the input path is null + */ + public static Path normalizePath(Path path) { + if (path != null) { + path = path.normalize(); + if (path.isAbsolute()) + return path; + + if (path.startsWith("~")) { + path = Path.of(System.getProperty("user.home")).resolve(path.subpath(1, path.getNameCount())); + return path; + } + } + + return path; + } + + /** + * Returns the system-wide configuration directory for the current platform. + *

+ * This directory is typically used for configuration files that apply to all users + * on the system and require administrative privileges to modify. + * + *

+ * Platform-specific locations: + *

    + *
  • Windows: %ProgramData% (e.g., {@code C:\\ProgramData})
  • + *
  • Linux: {@code /etc}
  • + *
  • macOS: {@code /Library/Preferences} (follows macOS conventions for system-level config)
  • + *
+ * + * @return the system-wide configuration directory as a {@link Path} + */ + public static Path getSystemConfigDir() { + String osName = System.getProperty("os.name").toLowerCase(); + if (osName.startsWith("windows")) { + return Path.of(System.getenv("ProgramData")); + } else if (osName.startsWith("mac")) { + return Path.of("/Library/Preferences"); + } else { + // Unix like OS + return Path.of("/etc"); + } + } + + /** + * Returns the system-local configuration directory for locally installed software. + *

+ * This directory is typically used for configuration files of software installed locally + * (not part of the base OS distribution) that apply to all users on the system. + * + *

+ * Platform-specific locations: + *

    + *
  • Windows: %ProgramData% (e.g., {@code C:\\ProgramData})
  • + *
  • Linux: {@code /usr/local/etc}
  • + *
  • macOS: {@code /Library/Application Support} (follows macOS conventions for system-level config)
  • + *
+ * + * @return the system-local configuration directory as a {@link Path} + */ + public static Path getSystemLocalConfigDir() { + String osName = System.getProperty("os.name").toLowerCase(); + if (osName.startsWith("windows")) { + return Path.of(System.getenv("ProgramData")); + } else if (osName.startsWith("mac")) { + return Path.of("/Library/Application Support"); + } else { + // Unix like OS + return Path.of("/usr/local/etc"); + } + } + + /** + * Returns the per-user configuration directory for the current platform. + *

+ * This directory is used for storing user-specific configuration files that don't + * require administrative privileges to modify. On Unix-like systems, this follows + * the XDG Base Directory specification. + * + *

+ * Platform-specific locations: + *

    + *
  • Windows: %APPDATA% (e.g., {@code C:\\Users\\username\\AppData\\Roaming})
  • + *
  • Linux: {@code ~/.config}
  • + *
  • macOS: {@code ~/.config} (intentionally using XDG style instead of {@code ~/Library/Preferences})
  • + *
+ * + * @return the per-user configuration directory as a {@link Path} + */ + public static Path getUserConfigDir() { + String osName = System.getProperty("os.name").toLowerCase(); + if (osName.startsWith("windows")) { + return Path.of(System.getenv("APPDATA")); + } else if (osName.startsWith("mac")) { + // return Path.of(System.getProperty("user.home"), "Library/Preferences"); + return Path.of(System.getProperty("user.home"), ".config"); + } else { + // Unix like OS + return Path.of(System.getProperty("user.home"), ".config"); + } + } + + /** + * Returns the system-wide persistent data directory for the current platform. + *

+ * This directory is typically used for storing application data that applies to all + * users on the system, such as databases, caches, and other persistent state that + * requires administrative privileges to modify. + * + *

+ * Platform-specific locations: + *

    + *
  • Windows: %ProgramData% (e.g., {@code C:\\ProgramData})
  • + *
  • Linux: {@code /var/lib}
  • + *
  • macOS: {@code /Library/Application Support} (follows macOS conventions for system-level data)
  • + *
+ * + * @return the system-wide persistent data directory as a {@link Path} + */ + public static Path getSystemDataDir() { + String osName = System.getProperty("os.name").toLowerCase(); + if (osName.startsWith("windows")) { + return Path.of(System.getenv("ProgramData")); + } else if (osName.startsWith("mac")) { + return Path.of("/Library/Application Support"); + } else { + // Unix like OS + return Path.of("/var/lib"); + } + } + + /** + * Returns the per-user persistent data directory for the current platform. + *

+ * This directory is used for storing user-specific application data such as databases, + * caches, and other persistent state. On Unix-like systems, this follows the XDG Base + * Directory specification. + * + *

+ * Platform-specific locations: + *

    + *
  • Windows: %LOCALAPPDATA% (e.g., {@code C:\\Users\\username\\AppData\\Local})
  • + *
  • Linux: {@code ~/.local/share}
  • + *
  • macOS: {@code ~/.local/share} (intentionally using XDG style instead of {@code ~/Library/Application Support})
  • + *
+ * + * @return the per-user persistent data directory as a {@link Path} + */ + public static Path getUserDataDir() { + String osName = System.getProperty("os.name").toLowerCase(); + if (osName.startsWith("windows")) { + return Path.of(System.getenv("LOCALAPPDATA")); + } else if (osName.startsWith("mac")) { + //return Path.of(System.getProperty("user.home"), "Library/Application Support"); + return Path.of(System.getProperty("user.home"), ".local/share"); + } else { + // Unix like OS + return Path.of(System.getProperty("user.home"), ".local/share"); + } + } } \ No newline at end of file diff --git a/api/src/main/java/io/bosonnetwork/utils/Json.java b/api/src/main/java/io/bosonnetwork/utils/Json.java index 340e45b..b7bb84f 100644 --- a/api/src/main/java/io/bosonnetwork/utils/Json.java +++ b/api/src/main/java/io/bosonnetwork/utils/Json.java @@ -27,6 +27,7 @@ import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Base64; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -89,6 +90,11 @@ public class Json { private static final String BOSON_JSON_MODULE_NAME = "io.bosonnetwork.utils.json.module"; + /** The pre-configured Jackson {@link ObjectMapper} instance for JSON serialization and deserialization. */ + public static final Base64.Encoder BASE64_ENCODER = Base64.getUrlEncoder().withoutPadding(); + /** The pre-configured Jackson {@link ObjectMapper} instance for JSON serialization and deserialization. */ + public static final Base64.Decoder BASE64_DECODER = Base64.getUrlDecoder(); + private static TypeReference> _mapType; private static SimpleModule _bosonJsonModule; diff --git a/api/src/main/java/io/bosonnetwork/utils/logging/HighlightingCompositeConverter.java b/api/src/main/java/io/bosonnetwork/utils/logging/HighlightingCompositeConverter.java index 053d52b..0464af5 100644 --- a/api/src/main/java/io/bosonnetwork/utils/logging/HighlightingCompositeConverter.java +++ b/api/src/main/java/io/bosonnetwork/utils/logging/HighlightingCompositeConverter.java @@ -1,3 +1,25 @@ +/* + * 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.utils.logging; import ch.qos.logback.classic.Level; diff --git a/api/src/main/java/io/bosonnetwork/vertx/BosonVerticle.java b/api/src/main/java/io/bosonnetwork/vertx/BosonVerticle.java index 86eefee..6549521 100644 --- a/api/src/main/java/io/bosonnetwork/vertx/BosonVerticle.java +++ b/api/src/main/java/io/bosonnetwork/vertx/BosonVerticle.java @@ -22,7 +22,6 @@ package io.bosonnetwork.vertx; -import java.util.List; import java.util.concurrent.Callable; import io.vertx.core.Context; @@ -108,15 +107,6 @@ public final JsonObject vertxConfig() { return vertxContext.config(); } - /** - * Returns the process arguments for the current Vert.x instance. - * - * @return a list of process arguments - */ - public final List processArgs() { - return vertxContext.processArgs(); - } - /** * Initializes the Verticle. *

diff --git a/api/src/main/java/io/bosonnetwork/vertx/VertxFuture.java b/api/src/main/java/io/bosonnetwork/vertx/VertxFuture.java index 776fc74..2cc9163 100644 --- a/api/src/main/java/io/bosonnetwork/vertx/VertxFuture.java +++ b/api/src/main/java/io/bosonnetwork/vertx/VertxFuture.java @@ -69,9 +69,7 @@ public class VertxFuture extends CompletableFuture implements java.util.co * @param future the Vert.x Future to wrap */ protected VertxFuture(Future future) { - this.future = future; - - future.andThen(ar -> { + this.future = future.andThen(ar -> { // update the internal state of CompletableFuture if (ar.succeeded()) super.complete(ar.result()); diff --git a/api/src/test/java/io/bosonnetwork/vertx/VertxFutureTests.java b/api/src/test/java/io/bosonnetwork/vertx/VertxFutureTests.java index ae86a13..17c18f8 100644 --- a/api/src/test/java/io/bosonnetwork/vertx/VertxFutureTests.java +++ b/api/src/test/java/io/bosonnetwork/vertx/VertxFutureTests.java @@ -217,22 +217,22 @@ void testVertxCompletableFutureGetInVertxContext(Vertx vertx, VertxTestContext c var ctx = vertx.getOrCreateContext(); Promise promise = Promise.promise(); - ctx.runOnContext(v -> { - try { - TimeUnit.MILLISECONDS.sleep(1900); - promise.complete("Foo bar"); - } catch (InterruptedException e) { - promise.fail(e); - } - }); + vertx.setTimer(2000, id -> promise.complete("Foo bar")); VertxFuture future = VertxFuture.of(promise.future()); ctx.runOnContext(v -> { printThreadContext("context.verify"); - IllegalStateException exception = assertThrows(IllegalStateException.class, future::get); - assertEquals("Cannot not be called on vertx thread or event loop thread", exception.getMessage()); - context.completeNow(); + context.verify(() -> { + IllegalStateException exception = assertThrows(IllegalStateException.class, future::get); + assertEquals("Cannot not be called on vertx thread or event loop thread", exception.getMessage()); + context.completeNow(); + }); + }); + + VertxFuture completedFuture = VertxFuture.succeededFuture("Foo bar"); + ctx.runOnContext(v -> { + context.verify(() -> assertEquals("Foo bar", completedFuture.join())); }); } } \ No newline at end of file diff --git a/api/src/test/resources/db/postgres/1_init_schema.sql b/api/src/test/resources/db/postgres/001_init_schema.sql similarity index 100% rename from api/src/test/resources/db/postgres/1_init_schema.sql rename to api/src/test/resources/db/postgres/001_init_schema.sql diff --git a/api/src/test/resources/db/postgres/2_add_index.sql b/api/src/test/resources/db/postgres/002_add_index.sql similarity index 100% rename from api/src/test/resources/db/postgres/2_add_index.sql rename to api/src/test/resources/db/postgres/002_add_index.sql diff --git a/api/src/test/resources/db/postgres/3_insert_sample_data.sql b/api/src/test/resources/db/postgres/003_insert_sample_data.sql similarity index 100% rename from api/src/test/resources/db/postgres/3_insert_sample_data.sql rename to api/src/test/resources/db/postgres/003_insert_sample_data.sql diff --git a/api/src/test/resources/db/postgres/4_add_profile_table.sql b/api/src/test/resources/db/postgres/004_add_profile_table.sql similarity index 100% rename from api/src/test/resources/db/postgres/4_add_profile_table.sql rename to api/src/test/resources/db/postgres/004_add_profile_table.sql diff --git a/api/src/test/resources/db/postgres/5_add_last_login_column.sql b/api/src/test/resources/db/postgres/005_add_last_login_column.sql similarity index 100% rename from api/src/test/resources/db/postgres/5_add_last_login_column.sql rename to api/src/test/resources/db/postgres/005_add_last_login_column.sql diff --git a/api/src/test/resources/db/postgres/6_normalize_email_case.sql b/api/src/test/resources/db/postgres/006_normalize_email_case.sql similarity index 100% rename from api/src/test/resources/db/postgres/6_normalize_email_case.sql rename to api/src/test/resources/db/postgres/006_normalize_email_case.sql diff --git a/api/src/test/resources/db/postgres/7_add_message_likes_table.sql b/api/src/test/resources/db/postgres/007_add_message_likes_table.sql similarity index 100% rename from api/src/test/resources/db/postgres/7_add_message_likes_table.sql rename to api/src/test/resources/db/postgres/007_add_message_likes_table.sql diff --git a/api/src/test/resources/db/postgres/8_add_view_user_messages_view.sql b/api/src/test/resources/db/postgres/008_add_view_user_messages_view.sql similarity index 100% rename from api/src/test/resources/db/postgres/8_add_view_user_messages_view.sql rename to api/src/test/resources/db/postgres/008_add_view_user_messages_view.sql diff --git a/api/src/test/resources/db/postgres/9_add_audit_log_table.sql b/api/src/test/resources/db/postgres/009_add_audit_log_table.sql similarity index 100% rename from api/src/test/resources/db/postgres/9_add_audit_log_table.sql rename to api/src/test/resources/db/postgres/009_add_audit_log_table.sql diff --git a/api/src/test/resources/db/postgres/10_add_trigger.sql b/api/src/test/resources/db/postgres/010_add_trigger.sql similarity index 100% rename from api/src/test/resources/db/postgres/10_add_trigger.sql rename to api/src/test/resources/db/postgres/010_add_trigger.sql diff --git a/api/src/test/resources/db/sqlite/1_init_schema.sql b/api/src/test/resources/db/sqlite/001_init_schema.sql similarity index 100% rename from api/src/test/resources/db/sqlite/1_init_schema.sql rename to api/src/test/resources/db/sqlite/001_init_schema.sql diff --git a/api/src/test/resources/db/sqlite/2_add_index.sql b/api/src/test/resources/db/sqlite/002_add_index.sql similarity index 100% rename from api/src/test/resources/db/sqlite/2_add_index.sql rename to api/src/test/resources/db/sqlite/002_add_index.sql diff --git a/api/src/test/resources/db/sqlite/3_insert_sample_data.sql b/api/src/test/resources/db/sqlite/003_insert_sample_data.sql similarity index 100% rename from api/src/test/resources/db/sqlite/3_insert_sample_data.sql rename to api/src/test/resources/db/sqlite/003_insert_sample_data.sql diff --git a/api/src/test/resources/db/sqlite/4_add_profile_table.sql b/api/src/test/resources/db/sqlite/004_add_profile_table.sql similarity index 100% rename from api/src/test/resources/db/sqlite/4_add_profile_table.sql rename to api/src/test/resources/db/sqlite/004_add_profile_table.sql diff --git a/api/src/test/resources/db/sqlite/5_add_last_login_column.sql b/api/src/test/resources/db/sqlite/005_add_last_login_column.sql similarity index 100% rename from api/src/test/resources/db/sqlite/5_add_last_login_column.sql rename to api/src/test/resources/db/sqlite/005_add_last_login_column.sql diff --git a/api/src/test/resources/db/sqlite/6_normalize_email_case.sql b/api/src/test/resources/db/sqlite/006_normalize_email_case.sql similarity index 100% rename from api/src/test/resources/db/sqlite/6_normalize_email_case.sql rename to api/src/test/resources/db/sqlite/006_normalize_email_case.sql diff --git a/api/src/test/resources/db/sqlite/7_add_message_likes_table.sql b/api/src/test/resources/db/sqlite/007_add_message_likes_table.sql similarity index 100% rename from api/src/test/resources/db/sqlite/7_add_message_likes_table.sql rename to api/src/test/resources/db/sqlite/007_add_message_likes_table.sql diff --git a/api/src/test/resources/db/sqlite/8_add_view_user_messages_view.sql b/api/src/test/resources/db/sqlite/008_add_view_user_messages_view.sql similarity index 100% rename from api/src/test/resources/db/sqlite/8_add_view_user_messages_view.sql rename to api/src/test/resources/db/sqlite/008_add_view_user_messages_view.sql diff --git a/api/src/test/resources/db/sqlite/9_add_audit_log_table.sql b/api/src/test/resources/db/sqlite/009_add_audit_log_table.sql similarity index 100% rename from api/src/test/resources/db/sqlite/9_add_audit_log_table.sql rename to api/src/test/resources/db/sqlite/009_add_audit_log_table.sql diff --git a/api/src/test/resources/db/sqlite/10_add_trigger.sql b/api/src/test/resources/db/sqlite/010_add_trigger.sql similarity index 100% rename from api/src/test/resources/db/sqlite/10_add_trigger.sql rename to api/src/test/resources/db/sqlite/010_add_trigger.sql diff --git a/cmds/src/main/java/io/bosonnetwork/am/AmCommand.java b/cmds/src/main/java/io/bosonnetwork/am/AmCommand.java index bbff9d3..bcf40fb 100644 --- a/cmds/src/main/java/io/bosonnetwork/am/AmCommand.java +++ b/cmds/src/main/java/io/bosonnetwork/am/AmCommand.java @@ -25,12 +25,14 @@ import java.io.IOException; import java.nio.file.Path; +import java.util.Map; import picocli.CommandLine.Option; import io.bosonnetwork.DefaultNodeConfiguration; import io.bosonnetwork.NodeConfiguration; import io.bosonnetwork.access.impl.AccessManager; +import io.bosonnetwork.utils.Json; /** * @hidden @@ -54,7 +56,8 @@ protected AccessManager getAccessManager() throws IOException { file = file.toAbsolutePath(); try { - builder.load(file); + Map map = Json.yamlMapper().readValue(configFile, Json.mapType()); + builder.template(map); } catch (Exception e) { System.out.println("Can not load the config file: " + configFile + ", error: " + e.getMessage()); e.printStackTrace(System.err); @@ -62,7 +65,7 @@ protected AccessManager getAccessManager() throws IOException { } NodeConfiguration config = builder.build(); - Path dataPath = config.dataPath(); + Path dataPath = config.dataDir(); if (dataPath == null) { System.out.println("No data path in the configuration."); System.exit(-1); diff --git a/cmds/src/main/java/io/bosonnetwork/launcher/Main.java b/cmds/src/main/java/io/bosonnetwork/launcher/Main.java index c9f4874..c448aed 100644 --- a/cmds/src/main/java/io/bosonnetwork/launcher/Main.java +++ b/cmds/src/main/java/io/bosonnetwork/launcher/Main.java @@ -48,7 +48,10 @@ import io.bosonnetwork.kademlia.KadNode; import io.bosonnetwork.service.BosonService; import io.bosonnetwork.service.BosonServiceException; +import io.bosonnetwork.service.ClientAuthenticator; +import io.bosonnetwork.service.ClientAuthorizer; import io.bosonnetwork.service.DefaultServiceContext; +import io.bosonnetwork.service.FederationAuthenticator; import io.bosonnetwork.service.ServiceContext; import io.bosonnetwork.utils.ApplicationLock; import io.bosonnetwork.utils.Json; @@ -109,10 +112,10 @@ private static ServiceConfig loadConfig(Path configFile) { } private static void loadServices() throws IOException { - if (config.dataPath() == null) + if (config.dataDir() == null) return; - Path servicesDir = config.dataPath().resolve("services"); + Path servicesDir = config.dataDir().resolve("services"); try (Stream stream = Files.list(servicesDir)) { stream.filter(Files::isRegularFile) .sorted(Main::compareConfigFileName) @@ -131,8 +134,10 @@ private static void loadService(ServiceConfig serviceConfig) { return; } - Path dataPath = config.dataPath() == null ? null : config.dataPath().resolve(svc.getId()).toAbsolutePath(); - ServiceContext ctx = new DefaultServiceContext(vertx, node, accessManager, serviceConfig.configuration, dataPath); + Path dataPath = config.dataDir() == null ? null : config.dataDir().resolve(svc.getId()).toAbsolutePath(); + ServiceContext ctx = new DefaultServiceContext(vertx, node, + ClientAuthenticator.allowAll(), ClientAuthorizer.noop(), + FederationAuthenticator.allowAll(), null, serviceConfig.configuration, dataPath); svc.init(ctx); System.out.format("Service %s[%s] is loaded.\n", svc.getName(), serviceConfig.className); @@ -190,7 +195,8 @@ private static void parseArgs(String[] args) { String configFile = args[++i]; try { - builder.load(configFile); + Map map = Json.yamlMapper().readValue(configFile, Json.mapType()); + builder.template(map); } catch (Exception e) { System.out.println("Can not load the config file: " + configFile + ", error: " + e.getMessage()); e.printStackTrace(System.err); @@ -223,7 +229,7 @@ private static void parseArgs(String[] args) { System.exit(-1); } - builder.dataPath(args[++i]); + builder.dataDir(args[++i]); } else if (args[i].equals("--bootstrap") || args[i].equals("-b")) { if (i + 1 >= args.length) { System.out.format("Missing the value for arg:%d %s\n", i, args[i]); @@ -294,8 +300,8 @@ public static void main(String[] args) { parseArgs(args); - Path lockFile = config.dataPath() != null ? - config.dataPath().resolve("lock") : + Path lockFile = config.dataDir() != null ? + config.dataDir().resolve("lock") : Path.of("./lock"); try (ApplicationLock lock = new ApplicationLock(lockFile)) { initBosonNode(); @@ -310,7 +316,7 @@ public static void main(String[] args) { } } catch (IOException | IllegalStateException e) { System.out.println("Another boson instance already running at " + - (config.dataPath() != null ? config.dataPath() : ".")); + (config.dataDir() != null ? config.dataDir() : ".")); } } } \ No newline at end of file diff --git a/cmds/src/main/java/io/bosonnetwork/shell/Main.java b/cmds/src/main/java/io/bosonnetwork/shell/Main.java index bd1c288..d73005c 100644 --- a/cmds/src/main/java/io/bosonnetwork/shell/Main.java +++ b/cmds/src/main/java/io/bosonnetwork/shell/Main.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Map; import java.util.concurrent.Callable; import org.jline.builtins.ConfigurationPath; @@ -57,6 +58,7 @@ import io.bosonnetwork.NodeInfo; import io.bosonnetwork.kademlia.KadNode; import io.bosonnetwork.utils.ApplicationLock; +import io.bosonnetwork.utils.Json; /** * @hidden @@ -204,7 +206,8 @@ private void parseArgs() throws IOException { if (configFile != null && (!saveConfig || Files.exists(Path.of(configFile)))) { try { - builder.load(configFile); + Map map = Json.yamlMapper().readValue(configFile, Json.mapType()); + builder.template(map); } catch (Exception e) { System.out.println("Can not load the config file: " + configFile + ", error: " + e.getMessage()); e.printStackTrace(System.err); @@ -222,17 +225,17 @@ private void parseArgs() throws IOException { builder.port(port); if (dataDir != null) { - builder.dataPath(dataDir); + builder.dataDir(dataDir); } else { - if (!builder.hasDataPath()) - builder.dataPath(DEFAULT_DATA_DIR); + if (!builder.hasDataDir()) + builder.dataDir(DEFAULT_DATA_DIR); } if (storageURL != null) { - builder.storageURL(storageURL); + builder.storageURI(storageURL); } else { - if (builder.hasDataPath()) - builder.storageURL("jdbc:sqlite:" + builder.dataPath().resolve("node.db")); + if (builder.hasDataDir()) + builder.storageURI("jdbc:sqlite:" + builder.dataDir().resolve("node.db")); } if (!builder.hasPrivateKey()) @@ -251,6 +254,8 @@ private void parseArgs() throws IOException { if (developerMode) builder.enableDeveloperMode(); + config = builder.build(); + if (saveConfig) { if (configFile == null && dataDir == null) { System.out.println("No config file and no data directory specified, can not save the configuration."); @@ -259,15 +264,14 @@ private void parseArgs() throws IOException { try { Path targetFile = configFile != null ? Path.of(configFile) : Path.of(dataDir).resolve("config.yaml"); - builder.save(targetFile); + Map map = ((DefaultNodeConfiguration) config).toMap(); + Json.yamlMapper().writeValue(targetFile.toFile(), map); } catch (Exception e) { System.out.println("Can not save the config file: " + configFile + ", error: " + e.getMessage()); e.printStackTrace(System.err); System.exit(-1); } } - - config = builder.build(); } private void initBosonNode() throws Exception { @@ -298,7 +302,7 @@ static KadNode getBosonNode() { } private void setLogOutput() { - Path logDir = config.dataPath() != null ? config.dataPath() : Path.of("").toAbsolutePath(); + Path logDir = config.dataDir() != null ? config.dataDir() : Path.of("").toAbsolutePath(); // with trailing slash System.setProperty("BOSON_LOG_DIR", logDir.toString() + File.separator); } @@ -310,7 +314,7 @@ public Integer call() throws Exception { initTerminal(); - Path lockFile = config.dataPath().resolve("lock"); + Path lockFile = config.dataDir().resolve("lock"); try (ApplicationLock lock = new ApplicationLock(lockFile)) { initBosonNode(); @@ -336,7 +340,7 @@ public Integer call() throws Exception { } } } catch (IOException | IllegalStateException e) { - System.out.println("Another boson instance already running at " + config.dataPath()); + System.out.println("Another boson instance already running at " + config.dataDir()); closeTerminal(); return -1; } diff --git a/dht/src/main/java/io/bosonnetwork/kademlia/KadNode.java b/dht/src/main/java/io/bosonnetwork/kademlia/KadNode.java index 1662bc4..c21bd1a 100644 --- a/dht/src/main/java/io/bosonnetwork/kademlia/KadNode.java +++ b/dht/src/main/java/io/bosonnetwork/kademlia/KadNode.java @@ -43,7 +43,6 @@ import io.bosonnetwork.kademlia.routing.KBucketEntry; import io.bosonnetwork.kademlia.security.Blacklist; import io.bosonnetwork.kademlia.storage.DataStorage; -import io.bosonnetwork.utils.Base58; import io.bosonnetwork.utils.Variable; import io.bosonnetwork.vertx.BosonVerticle; import io.bosonnetwork.vertx.VertxCaffeine; @@ -91,7 +90,7 @@ public KadNode(NodeConfiguration config) { } try { - Signature.KeyPair keyPair = Signature.KeyPair.fromPrivateKey(Base58.decode(config.privateKey())); + Signature.KeyPair keyPair = Signature.KeyPair.fromPrivateKey(config.privateKey()); this.identity = new CachedCryptoIdentity(keyPair, null); } catch (Exception e) { log.error("Invalid configuration: private key is invalid"); @@ -119,30 +118,26 @@ private void checkConfig(NodeConfiguration config) { if (config.bootstrapNodes() == null || config.bootstrapNodes().isEmpty()) log.warn("No bootstrap nodes are configured"); - Path dataPath = config.dataPath(); - if (dataPath != null) { - if (Files.exists(dataPath)) { - if (!Files.isDirectory(dataPath)) { - log.error("Data path {} is not a directory", dataPath); - throw new IllegalArgumentException("Data path " + dataPath + " is not a directory"); + Path dir = config.dataDir(); + if (dir != null) { + if (Files.exists(dir)) { + if (!Files.isDirectory(dir)) { + log.error("Data path {} is not a directory", dir); + throw new IllegalArgumentException("Data path " + dir + " is not a directory"); } } else { try { - Files.createDirectories(dataPath); + Files.createDirectories(dir); } catch (IOException e) { - log.error("Data path {} can not be created", dataPath); - throw new IllegalArgumentException("Data path " + dataPath + " can not be created", e); + log.error("Data path {} can not be created", dir); + throw new IllegalArgumentException("Data path " + dir + " can not be created", e); } } - - log.info("Using data path {}, persistent is enabled", dataPath); - } else { - log.warn("No data path is configured, persistent is disabled"); } - if (config.storageURL() != null) { - if (!DataStorage.supports(config.storageURL())) - throw new IllegalArgumentException("unsupported storage URL: " + config.storageURL()); + if (config.storageURI() != null) { + if (!DataStorage.supports(config.storageURI())) + throw new IllegalArgumentException("unsupported storage URL: " + config.storageURI()); } else { log.warn("No storage URL is configured, in-memory storage is used"); } @@ -202,7 +197,7 @@ else if (current instanceof ListenerArray listeners) @Override public synchronized VertxFuture start() { if (this.vertx != null) - throw new IllegalStateException("Already started"); + return VertxFuture.failedFuture(new IllegalStateException("Already started")); Future future = config.vertx().deployVerticle(this).mapEmpty(); return VertxFuture.of(future); @@ -211,7 +206,7 @@ public synchronized VertxFuture start() { @Override public VertxFuture stop() { if (!isRunning()) - throw new IllegalStateException("Not started"); + return VertxFuture.failedFuture(new IllegalStateException("Not started")); Promise promise = Promise.promise(); runOnContext(v -> { @@ -235,7 +230,15 @@ public void prepare(Vertx vertx, Context context) { @Override public Future deploy() { tokenManager = new TokenManager(); - storage = DataStorage.create(config.storageURL()); + + String storageURI = config.storageURI(); + // fix the sqlite database file location + if (storageURI.startsWith("jdbc:sqlite:")) { + Path dbFile = Path.of(storageURI.substring("jdbc:sqlite:".length())); + if (!dbFile.isAbsolute()) + storageURI = "jdbc:sqlite:" + config.dataDir().resolve(dbFile).toAbsolutePath(); + } + storage = DataStorage.create(storageURI); // TODO: empty blacklist for now blacklist = Blacklist.empty(); @@ -270,12 +273,10 @@ public void disconnected(Network network) { ArrayList> futures = new ArrayList<>(2); if (config.host4() != null) { dht4 = new DHT(identity, Network.IPv4, config.host4(), config.port(), config.bootstrapNodes(), - storage, tokenManager, blacklist, config.enableSuspiciousNodeDetector(), + storage, config.dataDir().resolve("dht4.cache"), + tokenManager, blacklist, config.enableSuspiciousNodeDetector(), config.enableSpamThrottling(), null, config.enableDeveloperMode()); - if (config.dataPath() != null) - dht4.enablePersistence(config.dataPath().resolve("dht4.cache")); - dht4.setConnectionStatusListener(listener); Future future = vertx.deployVerticle(dht4).andThen(ar -> { @@ -288,12 +289,10 @@ public void disconnected(Network network) { if (config.host6() != null) { dht6 = new DHT(identity, Network.IPv6, config.host6(), config.port(), config.bootstrapNodes(), - storage, tokenManager, blacklist, config.enableSuspiciousNodeDetector(), + storage, config.dataDir().resolve("dht6.cache"), + tokenManager, blacklist, config.enableSuspiciousNodeDetector(), config.enableSpamThrottling(), null, config.enableDeveloperMode()); - if (config.dataPath() != null) - dht6.enablePersistence(config.dataPath().resolve("dht6.cache")); - dht6.setConnectionStatusListener(listener); Future future = vertx.deployVerticle(dht6).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 4e4d74e..99fdde2 100644 --- a/dht/src/main/java/io/bosonnetwork/kademlia/impl/DHT.java +++ b/dht/src/main/java/io/bosonnetwork/kademlia/impl/DHT.java @@ -111,7 +111,7 @@ public class DHT extends BosonVerticle { private final RoutingTable routingTable; private long lastMaintenance; - private Path persistFile; + private final Path persistFile; private final List timers; @@ -124,7 +124,7 @@ public class DHT extends BosonVerticle { private static final Logger log = LoggerFactory.getLogger(DHT.class); public DHT(Identity identity, Network network, String host, int port, Collection bootstrapNodes, - DataStorage storage, TokenManager tokenManager, Blacklist blacklist, + DataStorage storage, Path persistFile, TokenManager tokenManager, Blacklist blacklist, boolean enableSuspiciousNodeDetector, boolean enableSpamThrottling, DHTMetrics metrics, boolean enableDeveloperMode) { this.identity = identity; @@ -132,6 +132,7 @@ public DHT(Identity identity, Network network, String host, int port, Collection this.host = host; this.port = port; this.storage = storage; + this.persistFile = persistFile; this.tokenManager = tokenManager; this.blacklist = blacklist; @@ -193,10 +194,6 @@ protected void setSibling(DHT dht) { this.sibling = dht; } - public void enablePersistence(Path persistFile) { - this.persistFile = persistFile; - } - public void setConnectionStatusListener(ConnectionStatusListener listener) { this.connectionStatusListener = listener; } diff --git a/dht/src/main/java/io/bosonnetwork/kademlia/impl/SimpleNodeConfiguration.java b/dht/src/main/java/io/bosonnetwork/kademlia/impl/SimpleNodeConfiguration.java index 4dcbff8..3f04a98 100644 --- a/dht/src/main/java/io/bosonnetwork/kademlia/impl/SimpleNodeConfiguration.java +++ b/dht/src/main/java/io/bosonnetwork/kademlia/impl/SimpleNodeConfiguration.java @@ -13,6 +13,7 @@ import io.bosonnetwork.NodeConfiguration; import io.bosonnetwork.NodeInfo; +import io.bosonnetwork.crypto.Signature; import io.bosonnetwork.kademlia.storage.InMemoryStorage; public class SimpleNodeConfiguration implements NodeConfiguration { @@ -20,8 +21,8 @@ public class SimpleNodeConfiguration implements NodeConfiguration { private final String host4; private final String host6; private final int port; - private final String privateKey; - private final Path dataPath; + private final Signature.PrivateKey privateKey; + private final Path dataDir; private final String storageURL; private final ArrayList bootstrapNodes; private final boolean enableSpamThrottling; @@ -34,8 +35,9 @@ public SimpleNodeConfiguration(NodeConfiguration config) { this.host6 = config.host6(); this.port = config.port(); this.privateKey = config.privateKey(); - this.dataPath = config.dataPath(); - this.storageURL = config.storageURL() != null ? config.storageURL() : InMemoryStorage.STORAGE_URI; + this.dataDir = config.dataDir() != null ? config.dataDir().toAbsolutePath() : + Path.of(System.getProperty("user.dir")).resolve("node"); + this.storageURL = config.storageURI() != null ? config.storageURI() : InMemoryStorage.STORAGE_URI; this.bootstrapNodes = new ArrayList<>(config.bootstrapNodes() != null ? config.bootstrapNodes() : Collections.emptyList()); this.enableSpamThrottling = config.enableSpamThrottling(); this.enableSuspiciousNodeDetector = config.enableSuspiciousNodeDetector(); @@ -83,17 +85,17 @@ public int port() { } @Override - public String privateKey() { + public Signature.PrivateKey privateKey() { return privateKey; } @Override - public Path dataPath() { - return dataPath; + public Path dataDir() { + return dataDir; } @Override - public String storageURL() { + public String storageURI() { return storageURL; } diff --git a/dht/src/main/resources/db/postgres/1_initial_schema.sql b/dht/src/main/resources/db/postgres/001_initial_schema.sql similarity index 100% rename from dht/src/main/resources/db/postgres/1_initial_schema.sql rename to dht/src/main/resources/db/postgres/001_initial_schema.sql diff --git a/dht/src/main/resources/db/sqlite/1_initial_schema.sql b/dht/src/main/resources/db/sqlite/001_initial_schema.sql similarity index 100% rename from dht/src/main/resources/db/sqlite/1_initial_schema.sql rename to dht/src/main/resources/db/sqlite/001_initial_schema.sql diff --git a/dht/src/main/resources/node.yaml b/dht/src/main/resources/node.yaml new file mode 100644 index 0000000..f759105 --- /dev/null +++ b/dht/src/main/resources/node.yaml @@ -0,0 +1,105 @@ +# ============================================================================ +# Sample configuration file for the Boson DHT Node +# +# Configuration file lookup order if no explicit path is provided: +# +# Unix-like systems: +# 1. ./node.yaml +# 2. ~/.config/boson/node.yaml +# 3. /usr/local/etc/boson/node.yaml +# 4. /etc/boson/node.yaml +# +# Windows: +# 1. .\node.yaml +# 2. %APPDATA%\boson\node.yaml +# 3. %ProgramData%\boson\node.yaml +# ============================================================================ + +# IPv4 address the DHT node will bind to for incoming connections. +# +# Not recommended: +# - 0.0.0.0 (wildcard bind) +# - 127.0.0.1 (loopback) +# +# The node should bind to a specific, stable address so it can be uniquely +# identified in the global DHT network. +host4: 192.168.8.16 + +# IPv6 address the DHT node will bind to. +# Comment out or remove this field if IPv6 is not required. +# +# Not recommended: +# - :: (wildcard bind) +# - ::1 (loopback) +# +# Use a specific, stable IPv6 address to ensure the node remains uniquely +# identifiable in the global DHT network. +# +# host6: 2001:db8:85a3::8a2e:370:7334 + +# Port number for the DHT node to listen on. +# Make sure this port is reachable from other nodes. +port: 39001 + +# Node private key. +# Supported formats: +# - Hex-encoded string (must start with "0x") +# - Base58-encoded string +# This key determines the node's long-term identity. Keep it secret. +privateKey: 0x751a9612f9bd80e6e37a77a704dc2a99dbfb162c35cb138ca46eaacd656de9bedfbc8cf0871986c0cd89d6f0f2e24ce3c50088d8a66b41d7ef5f456c1defde28 + +# Directory used by the node to store data such as routing tables, +# persistent caches, and database files. +# +# If omitted, defaults to the current working directory. +# +# Recommended locations for user deployments: +# - Unix-like: ~/.local/share/boson/node +# - Windows: %LOCALAPPDATA%\boson\node +# +# Recommended locations for system-wide deployments: +# - Unix-like: /var/lib/boson/node +# - Windows: %ProgramData%\boson\node +dataDir: ~/.local/share/boson/node + +# Storage backend used by the node. +# Supported values: +# - PostgreSQL: postgresql://user:password@host:port/database +# - SQLite: jdbc:sqlite:/path/to/sqlite.db +# +# For lightweight or embedded deployments, SQLite is recommended. +# For super node deployments, prefer PostgreSQL. +storageURL: jdbc:sqlite:node.db + +# Bootstrap nodes used when joining the DHT. +# Each bootstrap node is a node info triple: +# [ nodeId, ipAddress, port ] +# +# Node ID must be a valid Base58-encoded node identifier. +bootstraps: + - - 2dLbPsaySh9EGWwpgreYiLEPG3NDhaojj7DBBfSsRr6k + - 203.0.113.5 + - 39001 + - - 7jFV8w7eivjGEpaDu4V38EZ16CDhn4JutEdtGBWC67rF + - 198.51.100.8 + - 39001 + +# Enables spam throttling to protect against abusive or high-volume peers. +# Default: true +enableSpamThrottling: true + +# Enables detection of suspicious nodes (e.g., anomalous behaviors or patterns). +# Default: true +enableSuspiciousNodeDetector: true + +# Enables Prometheus-compatible metrics collection endpoint. +# Useful for monitoring and diagnostics. +# Default: false +enableMetrics: false + +# Enables Developer Mode. +# In this mode, the node accepts local/private network addresses +# (e.g., RFC1918 addresses) in its routing table. +# Do NOT use this in production environments. +# Default: false +enableDeveloperMode: false \ No newline at end of file diff --git a/dht/src/test/java/io/bosonnetwork/kademlia/NodeAsyncTests.java b/dht/src/test/java/io/bosonnetwork/kademlia/NodeAsyncTests.java index 00b1238..6712156 100644 --- a/dht/src/test/java/io/bosonnetwork/kademlia/NodeAsyncTests.java +++ b/dht/src/test/java/io/bosonnetwork/kademlia/NodeAsyncTests.java @@ -27,6 +27,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; import io.vertx.junit5.Timeout; import io.vertx.junit5.VertxExtension; @@ -48,6 +49,7 @@ import io.bosonnetwork.vertx.VertxFuture; @ExtendWith(VertxExtension.class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) @Timeout(value = NodeAsyncTests.TEST_NODES + 1, timeUnit = TimeUnit.MINUTES) public class NodeAsyncTests { static final int TEST_NODES = 32; @@ -68,8 +70,8 @@ private static VertxFuture startBootstrap() { .vertx(vertx) .address4(localAddr) .port(TEST_NODES_PORT_START - 1) - .dataPath(testDir.resolve("nodes" + File.separator + "node-bootstrap")) - .storageURL("jdbc:sqlite:" + testDir.resolve("nodes" + File.separator + "node-bootstrap" + File.separator + "storage.db")) + .dataDir(testDir.resolve("nodes" + File.separator + "node-bootstrap")) + .storageURI("jdbc:sqlite:" + testDir.resolve("nodes" + File.separator + "node-bootstrap" + File.separator + "storage.db")) .enableDeveloperMode() .build(); @@ -82,26 +84,36 @@ private static VertxFuture stopBootstrap() { return bootstrap.stop(); } - private static VertxFuture executeSequentially(int max, int index, Function> action) { - if (index >= max) - return VertxFuture.succeededFuture(); + private static VertxFuture executeSequentially(int count, Function> action) { + VertxFuture chain = VertxFuture.succeededFuture(); + for (int i = 0; i < count; i++) { + final int index = i; + chain = chain.thenCompose(v -> action.apply(index).whenComplete((r, e) -> { + if (e != null) { + System.err.println("Index " + index + " failed"); + //noinspection CallToPrintStackTrace + e.printStackTrace(); + } + })); + } - return action.apply(index) - .thenCompose(result -> executeSequentially(max, index + 1, action)); + return chain.thenApply(v -> null); } - protected static VertxFuture executeSequentially(List nodes, int index, Function> action) { - if (index >= nodes.size()) - return VertxFuture.succeededFuture(); + protected static VertxFuture executeSequentially(List nodes, Function> action) { + VertxFuture chain = VertxFuture.succeededFuture(); - var node = nodes.get(index); - return action.apply(node) - .thenCompose(v -> executeSequentially(nodes, index + 1, action)) - .exceptionally(e -> { + for (final KadNode node : nodes) { + chain = chain.thenCompose(v -> action.apply(node).whenComplete((r, e) -> { + if (e != null) { + System.err.println("Node " + node.getId() + " failed"); //noinspection CallToPrintStackTrace e.printStackTrace(); - return null; - }); + } + })); + } + + return chain; } private static VertxFuture createTestNode(int index) { @@ -111,8 +123,8 @@ private static VertxFuture createTestNode(int index) { .vertx(vertx) .address4(localAddr) .port(TEST_NODES_PORT_START + index) - .dataPath(testDir.resolve("nodes" + File.separator + "node-" + index)) - .storageURL("jdbc:sqlite:" + testDir.resolve("nodes" + File.separator + "node-" + index + File.separator + "storage.db")) + .dataDir(testDir.resolve("nodes" + File.separator + "node-" + index)) + .storageURI("jdbc:sqlite:" + testDir.resolve("nodes" + File.separator + "node-" + index + File.separator + "storage.db")) .addBootstrap(bootstrap.getNodeInfo().getV4()) .enableDeveloperMode() .build(); @@ -129,12 +141,11 @@ public void connected(Network network) { } }); - node.start(); - return VertxFuture.of(promise.future()); + return node.start().thenApply(v -> node); } private static VertxFuture startTestNodes() { - return executeSequentially(TEST_NODES, 0, NodeAsyncTests::createTestNode) + return executeSequentially(TEST_NODES, NodeAsyncTests::createTestNode) .whenComplete((v, e) -> { if (e == null) System.out.println("\n\n\007🟢 All the nodes are ready!!!"); @@ -146,7 +157,10 @@ private static VertxFuture startTestNodes() { private static VertxFuture stopTestNodes() { System.out.println("\n\n\007🟢 Stopping all the nodes ...\n"); // cannot stop all the nodes in parallel, it will cause vertx internal error. - return executeSequentially(testNodes, 0, KadNode::stop); + return executeSequentially(testNodes, n -> { + System.out.format("\n\n\007🟢 Stopping the node %s ...\n", n.getId()); + return n.stop(); + }); } private static VertxFuture dumpRoutingTable(String name, KadNode node) { @@ -187,12 +201,7 @@ private static VertxFuture dumpRoutingTables() { @BeforeAll @Timeout(value = TEST_NODES + 1, timeUnit = TimeUnit.MINUTES) static void setup(VertxTestContext context) throws Exception { - localAddr = AddressUtils.getAllAddresses() - .filter(Inet4Address.class::isInstance) - .filter(AddressUtils::isAnyUnicast) - .distinct() - .findFirst() - .orElse(null); + localAddr = AddressUtils.getDefaultRouteAddress(Inet4Address.class); if (localAddr == null) fail("No eligible address to run the test."); @@ -218,6 +227,7 @@ static void setup(VertxTestContext context) throws Exception { } @AfterAll + @Timeout(value = TEST_NODES + 1, timeUnit = TimeUnit.SECONDS) static void teardown(VertxTestContext context) throws Exception { dumpRoutingTables().thenCompose(v -> { return stopTestNodes(); @@ -244,7 +254,7 @@ void testNodeWithPresetKey(VertxTestContext context) { .address4(localAddr) .port(TEST_NODES_PORT_START - 100) .privateKey(keypair.privateKey().bytes()) - .dataPath(testDir.resolve("nodes" + File.separator + "node-" + nodeId)) + .dataDir(testDir.resolve("nodes" + File.separator + "node-" + nodeId)) .build(); var node = new KadNode(config); @@ -257,10 +267,10 @@ void testNodeWithPresetKey(VertxTestContext context) { @Test @Timeout(value = TEST_NODES, timeUnit = TimeUnit.MINUTES) void testFindNode(VertxTestContext context) { - executeSequentially(testNodes, 0, target -> { + executeSequentially(testNodes, target -> { System.out.format("\n\n\007🟢 Looking up node %s ...\n", target.getId()); - return executeSequentially(testNodes, 0, node -> { + return executeSequentially(testNodes, node -> { System.out.format("\n\n\007⌛ %s looking up node %s ...\n", node.getId(), target.getId()); var future = (VertxFuture>) node.findNode(target.getId()); return future.thenAccept(result -> { @@ -278,14 +288,14 @@ void testFindNode(VertxTestContext context) { @Test @Timeout(value = TEST_NODES, timeUnit = TimeUnit.MINUTES) void testAnnounceAndFindPeer(VertxTestContext context) { - executeSequentially(testNodes, 0, announcer -> { + executeSequentially(testNodes, announcer -> { var p = PeerInfo.create(announcer.getId(), 8888); System.out.format("\n\n\007🟢 %s announce peer %s ...\n", announcer.getId(), p.getId()); return ((VertxFuture)announcer.announcePeer(p)).thenCompose(v -> { System.out.format("\n\n\007🟢 Looking up peer %s ...\n", p.getId()); - return executeSequentially(testNodes, 0, node -> { + return executeSequentially(testNodes, node -> { System.out.format("\n\n\007⌛ %s looking up peer %s ...\n", node.getId(), p.getId()); var future = (VertxFuture>) node.findPeer(p.getId(), 0); return future.thenAccept(result -> { @@ -305,14 +315,14 @@ void testAnnounceAndFindPeer(VertxTestContext context) { @Test @Timeout(value = TEST_NODES, timeUnit = TimeUnit.MINUTES) void testStoreAndFindValue(VertxTestContext context) { - executeSequentially(testNodes, 0, announcer -> { + executeSequentially(testNodes, announcer -> { var v = Value.createValue(("Hello from " + announcer.getId()).getBytes()); System.out.format("\n\n\007🟢 %s store value %s ...\n", announcer.getId(), v.getId()); return ((VertxFuture) announcer.storeValue(v)).thenCompose(na -> { System.out.format("\n\n\007🟢 Looking up value %s ...\n", v.getId()); - return executeSequentially(testNodes, 0, node -> { + return executeSequentially(testNodes, node -> { System.out.format("\n\n\007⌛ %s looking up value %s ...\n", node.getId(), v.getId()); var future = (VertxFuture) node.findValue(v.getId()); return future.thenAccept(result -> { @@ -333,7 +343,7 @@ void testUpdateAndFindSignedValue(VertxTestContext context) { var values = new ArrayList(TEST_NODES); // initial announcement - executeSequentially(testNodes, 0, announcer -> { + executeSequentially(testNodes, announcer -> { var peerKeyPair = KeyPair.random(); var nonce = Nonce.random(); final Value v; @@ -348,7 +358,7 @@ void testUpdateAndFindSignedValue(VertxTestContext context) { System.out.format("\n\n\007🟢 %s store value %s ...\n", announcer.getId(), v.getId()); return ((VertxFuture)announcer.storeValue(v)).thenCompose(na -> { System.out.format("\n\n\007🟢 Looking up value %s ...\n", v.getId()); - return executeSequentially(testNodes, 0, node -> { + return executeSequentially(testNodes, node -> { System.out.format("\n\n\007⌛ %s looking up value %s ...\n", node.getId(), v.getId()); var future = (VertxFuture) node.findValue(v.getId()); return future.thenAccept(result -> { @@ -366,7 +376,7 @@ void testUpdateAndFindSignedValue(VertxTestContext context) { }); }).thenCompose(unused -> { // update announcement - return executeSequentially(testNodes.size(), 0, index -> { + return executeSequentially(testNodes.size(), index -> { KadNode announcer = testNodes.get(index); final Value v; try { @@ -380,7 +390,7 @@ void testUpdateAndFindSignedValue(VertxTestContext context) { System.out.format("\n\n\007🟢 %s update value %s ...\n", announcer.getId(), v.getId()); return ((VertxFuture) announcer.storeValue(v)).thenCompose(unused1 -> { System.out.format("\n\n\007🟢 Looking up value %s ...\n", v.getId()); - return executeSequentially(testNodes, 0, node -> { + return executeSequentially(testNodes, node -> { System.out.format("\n\n\007⌛ %s looking up value %s ...\n", node.getId(), v.getId()); return ((VertxFuture) node.findValue(v.getId())).thenAccept(result -> { System.out.format("\007🟢 %s lookup value %s finished\n", node.getId(), v.getId()); @@ -404,7 +414,7 @@ void testUpdateAndFindEncryptedValue(VertxTestContext context) { var recipients = new ArrayList(TEST_NODES); // initial announcement - executeSequentially(testNodes, 0, announcer -> { + executeSequentially(testNodes, announcer -> { var recipient = KeyPair.random(); recipients.add(recipient); @@ -423,7 +433,7 @@ void testUpdateAndFindEncryptedValue(VertxTestContext context) { System.out.format("\n\n\007🟢 %s store value %s ...\n", announcer.getId(), v.getId()); return ((VertxFuture) announcer.storeValue(v)).thenCompose(unused -> { System.out.format("\n\n\007🟢 Looking up value %s ...\n", v.getId()); - return executeSequentially(testNodes, 0, node -> { + return executeSequentially(testNodes, node -> { System.out.format("\n\n\007⌛ %s looking up value %s ...\n", node.getId(), v.getId()); return ((VertxFuture) node.findValue(v.getId())).thenAccept(result -> { System.out.format("\007🟢 %s lookup value %s finished\n", node.getId(), v.getId()); @@ -444,7 +454,7 @@ void testUpdateAndFindEncryptedValue(VertxTestContext context) { }); }).thenCompose(unused -> { // update announcement - return executeSequentially(testNodes.size(), 0, index -> { + return executeSequentially(testNodes.size(), index -> { KadNode announcer = testNodes.get(index); var recipient = recipients.get(index); var data = ("Updated value from " + announcer.getId()).getBytes(); @@ -460,7 +470,7 @@ void testUpdateAndFindEncryptedValue(VertxTestContext context) { System.out.format("\n\n\007🟢 %s update value %s ...\n", announcer.getId(), v.getId()); return ((VertxFuture) announcer.storeValue(v)).thenCompose(unused1 -> { System.out.format("\n\n\007🟢 Looking up value %s ...\n", v.getId()); - return executeSequentially(testNodes, 0, node -> { + return executeSequentially(testNodes, node -> { System.out.format("\n\n\007⌛ %s looking up value %s ...\n", node.getId(), v.getId()); return ((VertxFuture) node.findValue(v.getId())).thenAccept(result -> { System.out.format("\007🟢 %s lookup value %s finished\n", node.getId(), v.getId()); diff --git a/dht/src/test/java/io/bosonnetwork/kademlia/NodeSyncTests.java b/dht/src/test/java/io/bosonnetwork/kademlia/NodeSyncTests.java index 190967c..e36f235 100644 --- a/dht/src/test/java/io/bosonnetwork/kademlia/NodeSyncTests.java +++ b/dht/src/test/java/io/bosonnetwork/kademlia/NodeSyncTests.java @@ -58,8 +58,8 @@ private static void startBootstrap() throws Exception { .vertx(vertx) .address4(localAddr) .port(TEST_NODES_PORT_START - 1) - .dataPath(testDir.resolve("nodes" + File.separator + "node-bootstrap")) - .storageURL("jdbc:sqlite:" + testDir.resolve("nodes" + File.separator + "node-bootstrap" + File.separator + "storage.db")) + .dataDir(testDir.resolve("nodes" + File.separator + "node-bootstrap")) + .storageURI("jdbc:sqlite:" + testDir.resolve("nodes" + File.separator + "node-bootstrap" + File.separator + "storage.db")) .enableDeveloperMode() .build(); @@ -80,8 +80,8 @@ private static void startTestNodes() throws Exception { .vertx(vertx) .address4(localAddr) .port(TEST_NODES_PORT_START + i) - .dataPath(testDir.resolve("nodes" + File.separator + "node-" + i)) - .storageURL("jdbc:sqlite:" + testDir.resolve("nodes" + File.separator + "node-" + i + File.separator + "storage.db")) + .dataDir(testDir.resolve("nodes" + File.separator + "node-" + i)) + .storageURI("jdbc:sqlite:" + testDir.resolve("nodes" + File.separator + "node-" + i + File.separator + "storage.db")) .addBootstrap(bootstrap.getNodeInfo().getV4()) .enableDeveloperMode() .build(); @@ -135,12 +135,7 @@ private static void dumpRoutingTables() throws Exception { @BeforeAll @Timeout(value = TEST_NODES + 1, unit = TimeUnit.MINUTES) static void setup() throws Exception { - localAddr = AddressUtils.getAllAddresses() - .filter(Inet4Address.class::isInstance) - .filter(AddressUtils::isAnyUnicast) - .distinct() - .findFirst() - .orElse(null); + localAddr = AddressUtils.getDefaultRouteAddress(Inet4Address.class); if (localAddr == null) fail("No eligible address to run the test."); @@ -185,7 +180,7 @@ void testNodeWithPresetKey() throws Exception { .address4(localAddr) .port(TEST_NODES_PORT_START - 100) .privateKey(keypair.privateKey().bytes()) - .dataPath(testDir.resolve("nodes" + File.separator + "node-" + nodeId)) + .dataDir(testDir.resolve("nodes" + File.separator + "node-" + nodeId)) .build(); var node = new KadNode(config); diff --git a/dht/src/test/java/io/bosonnetwork/kademlia/SybilTests.java b/dht/src/test/java/io/bosonnetwork/kademlia/SybilTests.java index a2ad597..2fd6afa 100644 --- a/dht/src/test/java/io/bosonnetwork/kademlia/SybilTests.java +++ b/dht/src/test/java/io/bosonnetwork/kademlia/SybilTests.java @@ -62,12 +62,7 @@ public class SybilTests { private KadNode target; private NodeInfo targetInfo; - private static final InetAddress localAddr = AddressUtils.getAllAddresses() - .filter(Inet4Address.class::isInstance) - .filter(AddressUtils::isAnyUnicast) - .distinct() - .findFirst() - .orElse(null); + private static final InetAddress localAddr = AddressUtils.getDefaultRouteAddress(Inet4Address.class); @BeforeEach void setUp() throws Exception { @@ -84,8 +79,8 @@ void setUp() throws Exception { .address4(localAddr) .port(39001) .generatePrivateKey() - .dataPath(testDir.resolve("nodes" + File.separator + "node-target")) - .storageURL("jdbc:sqlite:" + testDir.resolve("nodes" + File.separator + "node-target" + File.separator + "storage.db")) + .dataDir(testDir.resolve("nodes" + File.separator + "node-target")) + .storageURI("jdbc:sqlite:" + testDir.resolve("nodes" + File.separator + "node-target" + File.separator + "storage.db")) .enableDeveloperMode() .build()); target.start().get(); @@ -116,7 +111,7 @@ void TestAddresses() throws Exception { .address4(localAddr) .port(39002 + i) .privateKey(sybilKey) - .dataPath(testDir.resolve("nodes" + File.separator + "node-" + i)) + .dataDir(testDir.resolve("nodes" + File.separator + "node-" + i)) .enableDeveloperMode() .build(); @@ -179,7 +174,7 @@ void TestIds() throws Exception { .generatePrivateKey() .address4(localAddr) .port(39002) - .dataPath(testDir.resolve("nodes" + File.separator + "node-" + i)) + .dataDir(testDir.resolve("nodes" + File.separator + "node-" + i)) .enableDeveloperMode() .build(); 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 ec4c92f..3ee0795 100644 --- a/dht/src/test/java/io/bosonnetwork/kademlia/rpc/RPCServerTests.java +++ b/dht/src/test/java/io/bosonnetwork/kademlia/rpc/RPCServerTests.java @@ -26,7 +26,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.net.Inet4Address; -import java.net.InetAddress; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -73,13 +72,7 @@ public class RPCServerTests { private static final Faker faker = new Faker(); - private static final String localAddr = AddressUtils.getAllAddresses() - .filter(Inet4Address.class::isInstance) - .filter(AddressUtils::isAnyUnicast) - .distinct() - .findFirst() - .map(InetAddress::getHostAddress) - .orElse(null); + private static final String localAddr = AddressUtils.getDefaultRouteAddress(Inet4Address.class).getHostAddress(); private final static Map values = new HashMap<>(); private final static Map> peers = new HashMap<>();