From 99f42acfc37037bee50eaecb24a89fc8d9c3a34e Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Sun, 11 Jan 2026 10:57:49 +0100 Subject: [PATCH 01/35] chore: Migrate to publish plugin --- .gitignore | 2 +- build.gradle | 69 +++++++++++++++++++------------------------------ settings.gradle | 9 +++++++ 3 files changed, 36 insertions(+), 44 deletions(-) diff --git a/.gitignore b/.gitignore index 332d4ac..a90b8b9 100644 --- a/.gitignore +++ b/.gitignore @@ -72,5 +72,5 @@ addons logs libraries -.env +.env.repo diff --git a/build.gradle b/build.gradle index 3fc669b..304d903 100644 --- a/build.gradle +++ b/build.gradle @@ -1,11 +1,7 @@ plugins { id 'java' id 'maven-publish' -} - -def env = new Properties() -file(".env").withInputStream { - env.load(it) + id "de.craftsblock.gradle.publish" version "0.0.20" } java { @@ -73,45 +69,32 @@ jar { } } -publishing { - publications { - normal(MavenPublication) { - artifactId "security" - from components.java - pom { - name = 'Security' - description = 'Protect your CraftsNet Restful API with an token-based access control system' - - scm { - url = 'https://github.com/CraftsBlock/CraftsNet-Security' - connection = 'scm:git:git://github.com/CraftsBlock/CraftsNet-Security.git' - developerConnection = 'scm:git:git@github.com:CraftsBlock/CraftsNet-Security.git' - } - - issueManagement { - system = 'github' - url = 'https://github.com/CraftsBlock/CraftsNet-Security/issues' - } - - licenses { - license { - name = 'GNU General Public License v3.0' - url = 'https://github.com/CraftsBlock/CraftsNet-Security/blob/master/LICENSE' - } - } - } +craftsPublish { + artifactId = "security" + name = "CraftsNet" + + component = project.components.java + + pom { + name = "Security" + description = "Protect your CraftsNet API with easy drop in security features." + + scm { + url = 'https://github.com/CraftsBlock/CraftsNet-Security' + connection = 'scm:git:git://github.com/CraftsBlock/CraftsNet-Security.git' + developerConnection = 'scm:git:git@github.com:CraftsBlock/CraftsNet-Security.git' } - } - repositories { - maven { - url('https://repo.craftsblock.de/experimental') - authentication { - basic(BasicAuthentication) - } - credentials { - username = env["username"] - password = env["password"] + + issueManagement { + system = 'github' + url = 'https://github.com/CraftsBlock/CraftsNet-Security/issues' + } + + licenses { + license { + name = 'GNU General Public License v3.0' + url = 'https://github.com/CraftsBlock/CraftsNet-Security/blob/master/LICENSE' } } } -} \ No newline at end of file +} diff --git a/settings.gradle b/settings.gradle index b9e61b5..b1a16a2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,11 @@ +pluginManagement { + repositories { + maven { + url = uri("https://repo.craftsblock.de/releases") + } + gradlePluginPortal() + } +} + rootProject.name = 'Security' From 39a866ec2b5e498f4954ff14d73464822501c6e8 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Wed, 14 Jan 2026 17:40:10 +0100 Subject: [PATCH 02/35] refactor: Rebuild auth system from scratch --- build.gradle | 18 +- .../modules/security/AddonEntrypoint.java | 56 ---- .../cnet/modules/security/CNetSecurity.java | 148 --------- .../modules/security/CraftsNetSecurity.java | 48 +++ .../modules/security/auth/AuthAdapter.java | 53 --- .../cnet/modules/security/auth/AuthChain.java | 130 ++++++++ .../security/auth/AuthChainManager.java | 20 -- .../modules/security/auth/AuthResult.java | 114 +++---- .../security/auth/adapter/AuthAdapter.java | 21 ++ .../security/auth/chains/AuthChain.java | 58 ---- .../security/auth/chains/SimpleAuthChain.java | 208 ------------ .../security/auth/event/AuthFailureEvent.java | 12 + .../security/auth/event/AuthResultEvent.java | 26 ++ .../security/auth/event/AuthSkipEvent.java | 12 + .../security/auth/event/AuthSuccessEvent.java | 12 + .../security/auth/exclusion/Exclusion.java | 13 + .../security/auth/exclusion/Exclusions.java | 110 +++++++ .../auth/exclusion/HttpExclusion.java | 17 + .../auth/exclusion/WebSocketExclusion.java | 14 + .../security/auth/listener/AuthListener.java | 40 +++ .../auth/listener/PreRequestListener.java | 58 ++++ .../listener/WebSocketConnectListener.java | 45 +++ .../modules/security/auth/token/Token.java | 89 ----- .../security/auth/token/TokenManager.java | 309 ------------------ .../security/auth/token/TokenPermission.java | 152 --------- .../auth/token/adapter/TokenAuthAdapter.java | 213 ------------ .../auth/token/adapter/TokenAuthType.java | 28 -- .../storage/FileTokenStorageDriver.java | 164 ---------- .../driver/storage/SQLTokenStorageDriver.java | 298 ----------------- .../driver/storage/TokenStorageDriver.java | 52 --- .../security/events/auth/AuthFailedEvent.java | 35 -- .../events/auth/AuthSuccessEvent.java | 35 -- .../events/auth/GenericAuthEvent.java | 15 - .../events/auth/GenericAuthResultEvent.java | 73 ----- .../auth/token/CancellableTokenEvent.java | 56 ---- .../events/auth/token/GenericTokenEvent.java | 44 --- .../events/auth/token/TokenCreateEvent.java | 33 -- .../events/auth/token/TokenRevokeEvent.java | 33 -- .../events/auth/token/TokenUsedEvent.java | 43 --- .../ratelimit/GenericRateLimitEvent.java | 27 -- .../ratelimit/RateLimitExceededEvent.java | 63 ---- .../listeners/PreRequestListener.java | 141 -------- .../security/listeners/StartupListener.java | 32 -- .../security/ratelimit/RateLimitAdapter.java | 164 ---------- .../security/ratelimit/RateLimitIndex.java | 98 ------ .../security/ratelimit/RateLimitInfo.java | 96 ------ .../security/ratelimit/RateLimitManager.java | 121 ------- .../ratelimit/builtin/IPRateLimitAdapter.java | 72 ---- .../builtin/TokenRateLimitAdapter.java | 78 ----- .../cnet/modules/security/utils/Entity.java | 26 -- .../cnet/modules/security/utils/Manager.java | 17 - src/main/resources/addon.json | 5 +- 52 files changed, 611 insertions(+), 3234 deletions(-) delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/AddonEntrypoint.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/CNetSecurity.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/AuthAdapter.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/AuthChain.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/AuthChainManager.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/adapter/AuthAdapter.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/chains/AuthChain.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/chains/SimpleAuthChain.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/event/AuthFailureEvent.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/event/AuthResultEvent.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/event/AuthSkipEvent.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/event/AuthSuccessEvent.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/exclusion/Exclusion.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/exclusion/Exclusions.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/exclusion/HttpExclusion.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/exclusion/WebSocketExclusion.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/listener/AuthListener.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/listener/PreRequestListener.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/listener/WebSocketConnectListener.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/token/Token.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/token/TokenManager.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/token/TokenPermission.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/token/adapter/TokenAuthAdapter.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/token/adapter/TokenAuthType.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/token/driver/storage/FileTokenStorageDriver.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/token/driver/storage/SQLTokenStorageDriver.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/auth/token/driver/storage/TokenStorageDriver.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/events/auth/AuthFailedEvent.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/events/auth/AuthSuccessEvent.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/events/auth/GenericAuthEvent.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/events/auth/GenericAuthResultEvent.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/CancellableTokenEvent.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/GenericTokenEvent.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/TokenCreateEvent.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/TokenRevokeEvent.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/TokenUsedEvent.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/events/ratelimit/GenericRateLimitEvent.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/events/ratelimit/RateLimitExceededEvent.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/listeners/PreRequestListener.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/listeners/StartupListener.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/ratelimit/RateLimitAdapter.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/ratelimit/RateLimitIndex.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/ratelimit/RateLimitInfo.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/ratelimit/RateLimitManager.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/ratelimit/builtin/IPRateLimitAdapter.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/ratelimit/builtin/TokenRateLimitAdapter.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/utils/Entity.java delete mode 100644 src/main/java/de/craftsblock/cnet/modules/security/utils/Manager.java diff --git a/build.gradle b/build.gradle index 304d903..bf10f6f 100644 --- a/build.gradle +++ b/build.gradle @@ -25,27 +25,15 @@ dependencies { // CraftsBlock dependencies ---------------------------------------------------------------------------------------- // https://repo.craftsblock.de/#/releases/de/craftsblock/craftscore/bom - implementation platform("de.craftsblock.craftscore:bom:3.8.12") - - // https://repo.craftsblock.de/#/releases/de/craftsblock/craftscore/event - implementation "de.craftsblock.craftscore:event" - - // https://repo.craftsblock.de/#/releases/de/craftsblock/craftscore/json - implementation "de.craftsblock.craftscore:json" - - // https://repo.craftsblock.de/#/releases/de/craftsblock/craftscore/sql - implementation "de.craftsblock.craftscore:sql" - - // https://repo.craftsblock.de/#/releases/de/craftsblock/craftscore/utils - implementation "de.craftsblock.craftscore:utils" + implementation platform("de.craftsblock.craftscore:bom:3.8.13-pre6") // https://repo.craftsblock.de/#/releases/de/craftsblock/craftsnet - implementation "de.craftsblock:craftsnet:3.5.6-pre6" + implementation "de.craftsblock:craftsnet:3.7.0-pre7" // Third party dependencies ---------------------------------------------------------------------------------------- // https://mvnrepository.com/artifact/org.springframework.security/spring-security-crypto - implementation 'org.springframework.security:spring-security-crypto:6.5.0' + implementation 'org.springframework.security:spring-security-crypto:7.0.2' // https://mvnrepository.com/artifact/org.jetbrains/annotations implementation 'org.jetbrains:annotations:26.0.2' diff --git a/src/main/java/de/craftsblock/cnet/modules/security/AddonEntrypoint.java b/src/main/java/de/craftsblock/cnet/modules/security/AddonEntrypoint.java deleted file mode 100644 index 17b2ab1..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/AddonEntrypoint.java +++ /dev/null @@ -1,56 +0,0 @@ -package de.craftsblock.cnet.modules.security; - -import de.craftsblock.cnet.modules.security.auth.AuthChainManager; -import de.craftsblock.cnet.modules.security.auth.chains.SimpleAuthChain; -import de.craftsblock.cnet.modules.security.auth.token.TokenManager; -import de.craftsblock.cnet.modules.security.ratelimit.RateLimitManager; -import de.craftsblock.craftsnet.addon.Addon; -import de.craftsblock.craftsnet.addon.meta.annotations.Meta; - -/** - * The AccessControllerAddon class extends the base {@link Addon} class to provide specific functionality - * for the access controller module. - * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.2 - * @since 1.0.0-SNAPSHOT - */ -@Meta(name = "CNetSecurity") -public class AddonEntrypoint extends Addon { - - /** - * Called when the addon is loaded. - */ - @Override - public void onLoad() { - // Set the instance - CNetSecurity.register(this); - CNetSecurity.register(this.logger()); - - // Set environment variables - CNetSecurity.register(new AuthChainManager()); - CNetSecurity.register(new TokenManager()); - CNetSecurity.register(new RateLimitManager()); - - // Create a new default auth chain - AuthChainManager chains = CNetSecurity.getAuthChainManager(); - if (chains != null) { - SimpleAuthChain chain = new SimpleAuthChain(); - chains.add(chain); - CNetSecurity.register(chain); - } - } - - /** - * Called when the addon is disabled. - */ - @Override - public void onDisable() { - CNetSecurity.getTokenManager().save(); - - // Unset the instance - CNetSecurity.unregister(this); - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/CNetSecurity.java b/src/main/java/de/craftsblock/cnet/modules/security/CNetSecurity.java deleted file mode 100644 index 939f812..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/CNetSecurity.java +++ /dev/null @@ -1,148 +0,0 @@ -package de.craftsblock.cnet.modules.security; - -import de.craftsblock.cnet.modules.security.auth.AuthChainManager; -import de.craftsblock.cnet.modules.security.auth.chains.SimpleAuthChain; -import de.craftsblock.cnet.modules.security.auth.token.TokenManager; -import de.craftsblock.cnet.modules.security.ratelimit.RateLimitManager; -import de.craftsblock.craftscore.event.Event; -import de.craftsblock.craftsnet.logging.Logger; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; - -import java.lang.reflect.InvocationTargetException; -import java.util.concurrent.ConcurrentHashMap; - -/** - * The AccessController class provides functionality for managing various variables used by the access control addon. - * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.1 - * @since 1.0.0-SNAPSHOT - */ -public class CNetSecurity { - - private static final ConcurrentHashMap, Object> instances = new ConcurrentHashMap<>(); - - /** - * Registers a new object instance in the internal map. The instance is stored using its class type as the key. - * This method is intended to be used internally to register object instances during initialization. - * - * @param instance The object instance to be registered. - */ - @ApiStatus.Internal - protected static void register(Object instance) { - instances.put(instance.getClass(), instance); - } - - /** - * Unregisters an object instance from the internal map. - * - * @param instance The object instance to be unregistered. - */ - @ApiStatus.Internal - protected static void unregister(Object instance) { - unregister(instance.getClass()); - } - - /** - * Unregisters an object type from the internal map. - * - * @param instance The object type to be unregistered. - */ - @ApiStatus.Internal - protected static void unregister(Class instance) { - instances.remove(instance); - } - - /** - * Retrieves a registered manager instance by its class type. - * If the requested manager has not been registered, {@code null} is returned. - * - * @param The type of the instance. - * @param type class type of the instance to be retrieved. - * @return The manager instance, if found. - */ - @ApiStatus.Internal - protected static @Nullable T get(Class type) { - if (!instances.containsKey(type)) return null; - return type.cast(instances.get(type)); - } - - /** - * Retrieves the currently set {@link AddonEntrypoint} instance. - * - * @return The current {@link AddonEntrypoint}, or null if none has been set. - */ - public static AddonEntrypoint getAddonEntrypoint() { - return get(AddonEntrypoint.class); - } - - /** - * Retrieves the default {@link SimpleAuthChain} instance. - * - * @return The {@link SimpleAuthChain} instance. - * @throws IllegalStateException If no default instance of {@link SimpleAuthChain} is registered. - */ - public static SimpleAuthChain getDefaultAuthChain() { - return get(SimpleAuthChain.class); - } - - /** - * Retrieves the {@link TokenManager} instance that manages authentication tokens. - * - * @return The {@link TokenManager} instance. - * @throws IllegalStateException If no instance of {@link TokenManager} is registered. - */ - public static TokenManager getTokenManager() { - return get(TokenManager.class); - } - - /** - * Retrieves the {@link AuthChainManager} instance that manages authentication chains. - * - * @return The {@link AuthChainManager} instance. - * @throws IllegalStateException If no instance of {@link AuthChainManager} is registered. - */ - public static AuthChainManager getAuthChainManager() { - return get(AuthChainManager.class); - } - - /** - * Retrieves the {@link RateLimitManager} instance that manages rate limits. - * - * @return The {@link RateLimitManager} instance. - * @throws IllegalStateException If no instance of {@link RateLimitManager} is registered. - */ - public static RateLimitManager getRateLimitManager() { - return get(RateLimitManager.class); - } - - /** - * Retrieves the {@link Logger} instance. - * - * @return The {@link Logger} instance. - * @throws IllegalStateException If no instance of {@link Logger} is registered. - */ - @ApiStatus.Internal - public static Logger getLogger() { - return get(Logger.class); - } - - /** - * Dispatches the given event to the registered listeners via the listener registry. - * This method ensures that the AccessController addon is active before proceeding. - * - * @param event The event to be dispatched to the listeners. - * @throws IllegalStateException If the AccessController addon is not active or not set. - * @throws InvocationTargetException If an error occurs while invoking a listener method. - * @throws IllegalAccessException If a listener method cannot be accessed. - */ - @ApiStatus.Internal - public static void callEvent(Event event) throws InvocationTargetException, IllegalAccessException { - if (getAddonEntrypoint() == null) - throw new IllegalStateException("The addon instance has not been set! Is the CNetSecurity addon active?"); - getAddonEntrypoint().craftsNet().listenerRegistry().call(event); - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java b/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java new file mode 100644 index 0000000..372a0b6 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java @@ -0,0 +1,48 @@ +package de.craftsblock.cnet.modules.security; + +import de.craftsblock.cnet.modules.security.auth.AuthChain; +import de.craftsblock.craftsnet.CraftsNet; +import de.craftsblock.craftsnet.addon.Addon; +import de.craftsblock.craftsnet.addon.meta.annotations.Meta; +import de.craftsblock.craftsnet.builder.ActivateType; + +import java.io.IOException; + +@Meta(name = "CNetSecurity") +public class CraftsNetSecurity extends Addon { + + private AuthChain authChain; + + public static void main(String[] args) throws IOException { + CraftsNet.create(CraftsNetSecurity.class) + .withWebServer(ActivateType.ENABLED) + .withWebSocketServer(ActivateType.ENABLED) + .withFileLogger(ActivateType.DISABLED) + .withDebug(true) + .build(); + } + + @Override + public void onLoad() { + this.authChain = new AuthChain(); + } + + @Override + public void onEnable() { + super.onEnable(); + } + + @Override + public void onDisable() { + super.onDisable(); + } + + public static AuthChain getAuthChain() { + return getInstance().authChain; + } + + public static CraftsNetSecurity getInstance() { + return getAddon(CraftsNetSecurity.class); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/AuthAdapter.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/AuthAdapter.java deleted file mode 100644 index 342be4d..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/auth/AuthAdapter.java +++ /dev/null @@ -1,53 +0,0 @@ -package de.craftsblock.cnet.modules.security.auth; - -import de.craftsblock.craftsnet.api.http.Exchange; -import de.craftsblock.craftsnet.api.http.Request; - -/** - * The {@link AuthAdapter} interface defines the contract for implementing custom authentication mechanisms. - * Classes implementing this interface provide the logic for authenticating requests and handling - * authentication success or failure. - * - *

It includes a method for performing authentication on a given {@link Request} and a default method - * for handling authentication failure by setting the appropriate state in an {@link AuthResult} object.

- * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @since 1.0.0-SNAPSHOT - */ -public interface AuthAdapter { - - /** - * Authenticates the incoming request. Implementations of this method should define the logic for - * checking whether the request is authorized or not. - * - * @param result The {@link AuthResult} object where the outcome of the authentication process is stored. - * @param exchange The {@link Exchange} object representing the HTTP request. - */ - void authenticate(AuthResult result, Exchange exchange); - - /** - * Marks the authentication process as failed. This method is used to set the failure state - * in the {@link AuthResult} object, including the reason for the failure. - * - * @param result The {@link AuthResult} object that stores the result of the authentication process. - * @param reason A string explaining why the authentication failed. - */ - default void failAuth(AuthResult result, String reason) { - result.cancel(reason); - } - - /** - * Marks the authentication process as failed. This method is used to set the failure state - * in the {@link AuthResult} object, including the reason for the failure. - * - * @param code The response http code. - * @param result The {@link AuthResult} object that stores the result of the authentication process. - * @param reason A string explaining why the authentication failed. - */ - default void failAuth(AuthResult result, int code, String reason) { - result.cancel(code, reason); - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/AuthChain.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/AuthChain.java new file mode 100644 index 0000000..356a723 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/auth/AuthChain.java @@ -0,0 +1,130 @@ +package de.craftsblock.cnet.modules.security.auth; + +import de.craftsblock.cnet.modules.security.auth.adapter.AuthAdapter; +import de.craftsblock.cnet.modules.security.auth.exclusion.Exclusions; +import de.craftsblock.craftsnet.api.BaseExchange; +import de.craftsblock.craftsnet.api.http.Exchange; +import de.craftsblock.craftsnet.api.http.Request; +import de.craftsblock.craftsnet.api.utils.Scheme; +import de.craftsblock.craftsnet.api.websocket.SocketExchange; +import de.craftsblock.craftsnet.api.websocket.WebSocketClient; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumMap; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; + +public class AuthChain { + + private final EnumMap> adapters = new EnumMap<>(Scheme.class); + + private final Exclusions exclusions = new Exclusions(); + + public void append(AuthAdapter adapter) { + computeApplicableAuthAdapterQueues(adapter).forEach(authAdapters -> { + synchronized (authAdapters) { + if (authAdapters.contains(adapter)) { + return; + } + + authAdapters.offer(adapter); + } + }); + } + + public void remove(AuthAdapter adapter) { + computeApplicableAuthAdapterQueues(adapter).forEach(authAdapters -> { + synchronized (authAdapters) { + if (!authAdapters.contains(adapter)) { + return; + } + + authAdapters.remove(adapter); + } + }); + } + + public AuthResult authenticate(BaseExchange exchange) { + if (exchange instanceof Exchange http) { + return authenticateHttp(http); + } else if (exchange instanceof SocketExchange webSocket) { + return authenticateWebSocket(webSocket); + } + + throw new IllegalStateException("Unexpected exchange: " + exchange.getClass().getName()); + } + + private AuthResult authenticateHttp(Exchange exchange) { + final Request request = exchange.request(); + if (this.exclusions.isHttpExcluded(request.getUrl(), request.getHttpMethod())) { + return AuthResult.skip(); + } + + Queue httpAdapters = this.computeAuthAdapterQueue(Scheme.HTTP); + synchronized (httpAdapters) { + for (AuthAdapter adapter : httpAdapters) { + if (!(adapter instanceof AuthAdapter.Http httpAuthAdapter)) { + throw new IllegalStateException("Found a non http auth adapter " + + adapter.getClass().getName() + " in the http adapter list!"); + } + + AuthResult result = httpAuthAdapter.authenticate(exchange); + if (result.isFailure()) { + return result; + } + } + } + + return AuthResult.ok(); + } + + private AuthResult authenticateWebSocket(SocketExchange exchange) { + final WebSocketClient client = exchange.client(); + if (this.exclusions.isWebSocketExcluded(client.getPath())) { + return AuthResult.skip(); + } + + Queue webSocketAdapters = this.computeAuthAdapterQueue(Scheme.WS); + synchronized (webSocketAdapters) { + for (AuthAdapter adapter : webSocketAdapters) { + if (!(adapter instanceof AuthAdapter.WebSocket webSocketAuthAdapter)) { + throw new IllegalStateException("Found a non web socket auth adapter " + + adapter.getClass().getName() + " in the web socket adapter list!"); + } + + AuthResult result = webSocketAuthAdapter.authenticate(exchange); + if (result.isFailure()) { + return result; + } + } + } + + return AuthResult.ok(); + } + + private Collection> computeApplicableAuthAdapterQueues(AuthAdapter adapter) { + Collection> authAdapters = new ArrayList<>(); + + if (adapter instanceof AuthAdapter.Http) { + authAdapters.add((computeAuthAdapterQueue(Scheme.HTTP))); + } + + if (adapter instanceof AuthAdapter.WebSocket) { + authAdapters.add(computeAuthAdapterQueue(Scheme.WS)); + } + + return authAdapters; + } + + private Queue computeAuthAdapterQueue(Scheme scheme) { + synchronized (adapters) { + return adapters.computeIfAbsent(scheme, s -> new LinkedBlockingQueue<>()); + } + } + + public Exclusions getExclusions() { + return exclusions; + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/AuthChainManager.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/AuthChainManager.java deleted file mode 100644 index d7d5d13..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/auth/AuthChainManager.java +++ /dev/null @@ -1,20 +0,0 @@ -package de.craftsblock.cnet.modules.security.auth; - -import de.craftsblock.cnet.modules.security.auth.chains.AuthChain; -import de.craftsblock.cnet.modules.security.utils.Manager; - -import java.util.concurrent.ConcurrentLinkedQueue; - -/** - * The {@code AuthChainManager} class is a manager for handling multiple {@link AuthChain} instances. - * It extends {@link ConcurrentLinkedQueue} to provide a thread-safe way to manage and manipulate - * authentication chains. Each {@link AuthChain} represents a chain of authentication adapters. - * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @since 1.0.0-SNAPSHOT - */ -public final class AuthChainManager extends ConcurrentLinkedQueue implements Manager { - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/AuthResult.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/AuthResult.java index bc95740..d7cf5cf 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/auth/AuthResult.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/auth/AuthResult.java @@ -1,89 +1,71 @@ package de.craftsblock.cnet.modules.security.auth; -/** - * The {@link AuthResult} class represents the outcome of an authentication process. - * It provides information about whether the authentication was successful or cancelled, - * and if cancelled, it holds a reason for the cancellation. - * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.1 - * @since 1.0.0-SNAPSHOT - */ public class AuthResult { - private boolean success = true; - private int code = 401; - private String cancelReason = ""; + private final Type type; + private final int code; + private final String reason; - /** - * Creates a new {@link AuthResult} instance with a default success state of {@code true}. - */ - public AuthResult() { + private AuthResult(Type type) { + this(type, null); } - /** - * Cancels the authentication, setting the success state to {@code false}. - */ - public void cancel() { - this.success = false; + private AuthResult(Type type, String reason) { + this(type, reason, 400); } - /** - * Cancels the authentication with a specific reason. - * - * @param reason The reason for cancellation, providing context for the failure. - */ - public void cancel(String reason) { - this.cancel(403, reason); - } - - /** - * Cancels the authentication with a specific reason. - * - * @param reason The reason for cancellation, providing context for the failure. - */ - public void cancel(int code, String reason) { - this.cancel(); - this.cancelReason = reason; + private AuthResult(Type type, String reason, int code) { + this.type = type; this.code = code; + this.reason = reason; } - /** - * Returns whether the authentication was successful or not. - * - * @return {@code true} if the authentication was successful, {@code false} otherwise. - */ - public boolean isSuccess() { - return success; + public boolean isOk() { + return this.type.equals(Type.OK); } - /** - * Returns whether the authentication process was cancelled. - * - * @return {@code true} if the process was cancelled, {@code false} otherwise. - */ - public boolean isCancelled() { - return !success; + public boolean isSkip() { + return this.type.equals(Type.SKIP); } - /** - * Returns the reason for cancelling the authentication process. - * If the authentication was successful, this will return an empty string. - * - * @return The cancellation reason or an empty string if authentication was successful. - */ - public String getCancelReason() { - return cancelReason; + public boolean isFailure() { + return this.type.equals(Type.FAILURE); } - /** - * Returns the http status code for cancelling the authentication process. - * - * @return The http status code. - */ public int getCode() { return code; } + public String getReason() { + return reason; + } + + public static AuthResult ok() { + return new AuthResult(Type.OK); + } + + public static AuthResult skip() { + return new AuthResult(Type.SKIP); + } + + public static AuthResult failure() { + return failure(null); + } + + public static AuthResult failure(String reason) { + return new AuthResult(Type.FAILURE, reason); + } + + public static AuthResult failure(String reason, int code) { + return new AuthResult(Type.FAILURE, reason, code); + } + + public enum Type { + + OK, + SKIP, + FAILURE + + } + } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/adapter/AuthAdapter.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/adapter/AuthAdapter.java new file mode 100644 index 0000000..a71111f --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/auth/adapter/AuthAdapter.java @@ -0,0 +1,21 @@ +package de.craftsblock.cnet.modules.security.auth.adapter; + +import de.craftsblock.cnet.modules.security.auth.AuthResult; +import de.craftsblock.craftsnet.api.http.Exchange; +import de.craftsblock.craftsnet.api.websocket.SocketExchange; + +public sealed interface AuthAdapter permits AuthAdapter.Http, AuthAdapter.WebSocket { + + non-sealed interface Http extends AuthAdapter { + + AuthResult authenticate(Exchange exchange); + + } + + non-sealed interface WebSocket extends AuthAdapter { + + AuthResult authenticate(SocketExchange exchange); + + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/chains/AuthChain.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/chains/AuthChain.java deleted file mode 100644 index 5871e41..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/auth/chains/AuthChain.java +++ /dev/null @@ -1,58 +0,0 @@ -package de.craftsblock.cnet.modules.security.auth.chains; - -import de.craftsblock.cnet.modules.security.auth.AuthAdapter; -import de.craftsblock.cnet.modules.security.auth.AuthResult; -import de.craftsblock.craftsnet.api.http.Exchange; - -/** - * The {@link AuthChain} class represents an authentication chain that manages multiple - * {@link AuthAdapter} instances. It provides methods to authenticate requests by passing - * them through the chain of adapters and managing the adapters dynamically. - * - *

This class is designed to be extended for custom implementations of authentication chains, - * where multiple authentication strategies (adapters) can be used in sequence.

- * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @since 1.0.0-SNAPSHOT - */ -public abstract class AuthChain { - - /** - * Authenticates the provided {@link Exchange} by passing it through the chain of registered - * {@link AuthAdapter} instances. Each adapter in the chain is responsible for determining - * whether the request is authorized or not. - * - * @param exchange The {@link Exchange} object representing the incoming HTTP request. - * @return The {@link AuthResult} object that contains the result of the authentication process. - */ - public abstract AuthResult authenticate(Exchange exchange); - - /** - * Appends a new {@link AuthAdapter} to the authentication chain. The adapter will be used - * during future authentication attempts. - * - * @param adapter The {@link AuthAdapter} to be added to the authentication chain. - * @return The instance of {@link AuthChain} used for chain method calls. - */ - public abstract AuthChain append(AuthAdapter adapter); - - /** - * Removes a specific {@link AuthAdapter} from the authentication chain. - * - * @param adapter The {@link AuthAdapter} to be removed from the authentication chain. - * @return The instance of {@link AuthChain} used for chain method calls. - */ - public abstract AuthChain remove(AuthAdapter adapter); - - /** - * Removes all {@link AuthAdapter} instances of the specified type from the authentication chain. - * This can be used to clear all adapters of a certain type (e.g., all token-based authenticators). - * - * @param adapter The class type of {@link AuthAdapter} to be removed. - * @return The instance of {@link AuthChain} used for chain method calls. - */ - public abstract AuthChain removeAll(Class adapter); - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/chains/SimpleAuthChain.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/chains/SimpleAuthChain.java deleted file mode 100644 index 622df5c..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/auth/chains/SimpleAuthChain.java +++ /dev/null @@ -1,208 +0,0 @@ -package de.craftsblock.cnet.modules.security.auth.chains; - -import de.craftsblock.cnet.modules.security.auth.AuthAdapter; -import de.craftsblock.cnet.modules.security.auth.AuthResult; -import de.craftsblock.craftsnet.api.http.Exchange; -import de.craftsblock.craftsnet.api.http.HttpMethod; -import de.craftsblock.craftsnet.api.http.Request; - -import java.util.*; -import java.util.concurrent.ConcurrentLinkedQueue; - -/** - * The {@link SimpleAuthChain} class is a concrete implementation of the {@link AuthChain} class, - * using a simple queue-based approach to handle multiple {@link AuthAdapter} instances in sequence. - * It processes each authentication adapter in the order they were added. - * - *

Adapters are executed in the order they were appended to the chain, and the chain stops - * processing if an authentication result is cancelled (i.e., if an adapter denies access).

- * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.1.0 - * @since 1.0.0-SNAPSHOT - */ -public class SimpleAuthChain extends AuthChain { - - private final ConcurrentLinkedQueue adapters = new ConcurrentLinkedQueue<>(); - private final List exclusions = new ArrayList<>(); - - /** - * Authenticates the provided {@link Exchange} by passing it through the chain of - * registered {@link AuthAdapter} instances. If any adapter in the chain cancels the - * authentication, the process stops. - * - * @param exchange The {@link Exchange} object representing the incoming HTTP request. - * @return The {@link AuthResult} object that contains the result of the authentication process. - */ - @Override - public AuthResult authenticate(final Exchange exchange) { - final Request request = exchange.request(); - final AuthResult result = new AuthResult(); - - if (exclusions.stream().anyMatch(exclusion -> exclusion.isExcluded(request))) - return result; - - // Iterate over each adapter in the chain and authenticate the request. - for (AuthAdapter adapter : adapters) { - adapter.authenticate(result, exchange); - - // Stop processing further adapters if the authentication is cancelled. - if (result.isCancelled()) break; - } - - return result; - } - - /** - * Appends a new {@link AuthAdapter} to the chain. If the adapter is already present, - * it will not be added again. - * - * @param adapter The {@link AuthAdapter} to be appended to the chain. - * @return The instance of {@link SimpleAuthChain} used for chain method calls. - */ - @Override - public SimpleAuthChain append(AuthAdapter adapter) { - if (!adapters.isEmpty() && adapters.contains(adapter)) return this; - adapters.add(adapter); - return this; - } - - /** - * Removes a specific {@link AuthAdapter} from the chain. - * - * @param adapter The {@link AuthAdapter} to be removed from the chain. - * @return The instance of {@link SimpleAuthChain} used for chain method calls. - */ - @Override - public SimpleAuthChain remove(AuthAdapter adapter) { - adapters.remove(adapter); - return this; - } - - /** - * Removes all instances of the specified {@link AuthAdapter} class from the chain. - * - * @param adapter The class type of the {@link AuthAdapter} to be removed. - * @return The instance of {@link SimpleAuthChain} used for chain method calls. - */ - @Override - public SimpleAuthChain removeAll(Class adapter) { - adapters.stream() - .filter(adapter::isInstance) - .forEach(this::remove); - return this; - } - - /** - * Adds an url pattern to be excluded from authentication. - * - * @param pattern A regular expression matching request urls to exclude. - * @return The instance of {@link SimpleAuthChain} used for chain method calls. - */ - public SimpleAuthChain addExclusion(String pattern) { - return addExclusion(pattern, HttpMethod.ALL); - } - - /** - * Adds an url pattern to be excluded from authentication for the specified http methods. - * - * @param pattern A regular expression matching request URLs to exclude. - * @param methods One or more {@link HttpMethod methods} for which the pattern should be excluded. - * @return The instance of {@link SimpleAuthChain} used for chain method calls. - */ - public SimpleAuthChain addExclusion(String pattern, HttpMethod... methods) { - exclusions.add(new Exclusion(pattern, normalizedMethods(methods))); - return this; - } - - /** - * Removes all exclusion entries matching the given url pattern. - *

Any {@link Exclusion} whose pattern equals the provided {@code pattern} will be removed

- * - * @param pattern The regular expression pattern of request URLs to remove from exclusions - * @return The current {@link SimpleAuthChain} instance, to allow method chaining - */ - public SimpleAuthChain removeExclusion(String pattern) { - return removeExclusion(pattern, HttpMethod.ALL); - } - - /** - * Removes exclusion entries matching the given url pattern for the specified http methods. - *

If an {@link Exclusion} with the same pattern exists and any of its methods matches one of - * the provided {@code methods}, that exclusion entry will be removed from the list.

- * - * @param pattern The regular expression pattern of request URLs to remove from exclusions - * @param methods One or more {@link HttpMethod methods} for which the pattern should no longer be excluded - * @return The current {@link SimpleAuthChain} instance, to allow method chaining - */ - public SimpleAuthChain removeExclusion(String pattern, HttpMethod... methods) { - exclusions.removeIf(exclusion -> { - if (!exclusion.pattern().equals(pattern)) return false; - - Collection excludedMethods = Arrays.asList(exclusion.methods()); - return Arrays.stream(methods).anyMatch(excludedMethods::contains); - }); - return this; - } - - /** - * Expands any composite {@link HttpMethod methods} into their - * constituent methods and returns a flat array of real methods. - * - * @param methods One or more {@link HttpMethod}s, possibly composite, to normalize. - * @return An array of individual {@link HttpMethod}s after expansion. - */ - private HttpMethod[] normalizedMethods(HttpMethod... methods) { - Set realMethods = new HashSet<>(); - - for (HttpMethod method : methods) - switch (method) { - case ALL, ALL_RAW -> { - List subMethods = Arrays.stream(method.getMethods()).toList(); - realMethods.addAll(subMethods); - } - default -> realMethods.add(method); - } - - return realMethods.toArray(HttpMethod[]::new); - } - - /** - * Internal record representing a URL pattern exclusion for one or more HTTP methods. - * - * @param pattern A regular expression for matching request URLs. - * @param methods The HTTP methods for which the pattern is excluded. - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @see HttpMethod - * @since 1.0.0-SNAPSHOT - */ - private record Exclusion(String pattern, HttpMethod... methods) { - - /** - * Checks whether the given {@link Request} matches this exclusion. - * - * @param request The incoming HTTP request to check. - * @return {@code true} if the request’s URL and method match this exclusion. - */ - boolean isExcluded(Request request) { - return isExcluded(request.getUrl(), request.getHttpMethod()); - } - - /** - * Checks whether the given URL and {@link HttpMethod} match this exclusion. - * - * @param url The request URL to match against the exclusion pattern. - * @param method The HTTP method to check for exclusion. - * @return {@code true} if the URL matches the pattern and the method is in the exclusion list. - */ - boolean isExcluded(String url, HttpMethod method) { - if (!url.matches(pattern)) return false; - return Arrays.asList(methods).contains(method); - } - - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/event/AuthFailureEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/event/AuthFailureEvent.java new file mode 100644 index 0000000..f0f1c10 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/auth/event/AuthFailureEvent.java @@ -0,0 +1,12 @@ +package de.craftsblock.cnet.modules.security.auth.event; + +import de.craftsblock.cnet.modules.security.auth.AuthResult; +import de.craftsblock.craftsnet.api.BaseExchange; + +public final class AuthFailureEvent extends AuthResultEvent { + + public AuthFailureEvent(BaseExchange exchange, AuthResult result) { + super(exchange, result); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/event/AuthResultEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/event/AuthResultEvent.java new file mode 100644 index 0000000..427e9bb --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/auth/event/AuthResultEvent.java @@ -0,0 +1,26 @@ +package de.craftsblock.cnet.modules.security.auth.event; + +import de.craftsblock.cnet.modules.security.auth.AuthResult; +import de.craftsblock.craftscore.event.Event; +import de.craftsblock.craftsnet.api.BaseExchange; + +public abstract sealed class AuthResultEvent extends Event + permits AuthFailureEvent, AuthSkipEvent, AuthSuccessEvent { + + private final BaseExchange exchange; + private final AuthResult result; + + public AuthResultEvent(BaseExchange exchange, AuthResult result) { + this.exchange = exchange; + this.result = result; + } + + public BaseExchange getExchange() { + return exchange; + } + + public AuthResult getResult() { + return result; + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/event/AuthSkipEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/event/AuthSkipEvent.java new file mode 100644 index 0000000..cc01028 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/auth/event/AuthSkipEvent.java @@ -0,0 +1,12 @@ +package de.craftsblock.cnet.modules.security.auth.event; + +import de.craftsblock.cnet.modules.security.auth.AuthResult; +import de.craftsblock.craftsnet.api.BaseExchange; + +public final class AuthSkipEvent extends AuthResultEvent { + + public AuthSkipEvent(BaseExchange exchange, AuthResult result) { + super(exchange, result); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/event/AuthSuccessEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/event/AuthSuccessEvent.java new file mode 100644 index 0000000..0be9d91 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/auth/event/AuthSuccessEvent.java @@ -0,0 +1,12 @@ +package de.craftsblock.cnet.modules.security.auth.event; + +import de.craftsblock.cnet.modules.security.auth.AuthResult; +import de.craftsblock.craftsnet.api.BaseExchange; + +public final class AuthSuccessEvent extends AuthResultEvent { + + public AuthSuccessEvent(BaseExchange exchange, AuthResult result) { + super(exchange, result); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/exclusion/Exclusion.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/exclusion/Exclusion.java new file mode 100644 index 0000000..8aacf19 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/auth/exclusion/Exclusion.java @@ -0,0 +1,13 @@ +package de.craftsblock.cnet.modules.security.auth.exclusion; + +import de.craftsblock.craftsnet.api.utils.Scheme; + +import java.util.regex.Pattern; + +public sealed interface Exclusion permits HttpExclusion, WebSocketExclusion { + + Scheme scheme(); + + Pattern path(); + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/exclusion/Exclusions.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/exclusion/Exclusions.java new file mode 100644 index 0000000..0c63358 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/auth/exclusion/Exclusions.java @@ -0,0 +1,110 @@ +package de.craftsblock.cnet.modules.security.auth.exclusion; + +import de.craftsblock.craftsnet.api.http.HttpMethod; +import de.craftsblock.craftsnet.api.utils.Scheme; +import org.intellij.lang.annotations.RegExp; + +import java.util.*; +import java.util.regex.Matcher; + +public final class Exclusions { + + private final Map> exclusions = new EnumMap<>(Scheme.class); + + public Exclusions http(@RegExp String path, HttpMethod... methods) { + Collection httpExclusions = exclusions.computeIfAbsent(Scheme.HTTP, s -> new ArrayList<>()); + + synchronized (httpExclusions) { + for (Exclusion exclusion : httpExclusions) { + if (!(exclusion instanceof HttpExclusion httpExclusion)) { + continue; + } + + if (!exclusion.path().pattern().equals(path)) { + throw new IllegalStateException("Found a non http exclusion " + + exclusion.getClass().getName() + " in the http list!"); + } + + if (httpExclusion.methods().containsAll(Arrays.asList(HttpMethod.normalize(methods)))) { + return this; + } + } + + httpExclusions.add(new HttpExclusion(path, methods)); + } + + return this; + } + + public boolean isHttpExcluded(String path, HttpMethod method) { + Collection httpExclusions = exclusions.get(Scheme.HTTP); + if (httpExclusions == null) { + return false; + } + + synchronized (httpExclusions) { + for (Exclusion exclusion : httpExclusions) { + if (!(exclusion instanceof HttpExclusion httpExclusion)) { + throw new IllegalStateException("Found a non http exclusion " + + exclusion.getClass().getName() + " in the http list!"); + } + + Matcher matcher = exclusion.path().matcher(path); + if (!matcher.matches()) { + continue; + } + + if (httpExclusion.methods().contains(method)) { + return true; + } + } + } + + return false; + } + + public Exclusions webSocket(@RegExp String path) { + Collection webSocketExclusions = exclusions.computeIfAbsent(Scheme.WS, s -> new ArrayList<>()); + + synchronized (webSocketExclusions) { + for (Exclusion exclusion : webSocketExclusions) { + if (!(exclusion instanceof WebSocketExclusion)) { + throw new IllegalStateException("Found a non web socket exclusion " + + exclusion.getClass().getName() + " in the web socket list!"); + } + + if (exclusion.path().pattern().equals(path)) { + return this; + } + } + + webSocketExclusions.add(new WebSocketExclusion(path)); + } + + return this; + } + + public boolean isWebSocketExcluded(String path) { + Collection httpExclusions = exclusions.get(Scheme.WS); + if (httpExclusions == null) { + return false; + } + + synchronized (httpExclusions) { + for (Exclusion exclusion : httpExclusions) { + if (!(exclusion instanceof WebSocketExclusion)) { + throw new IllegalStateException("Found a non web socket exclusion " + + exclusion.getClass().getName() + " in the web socket list!"); + } + + Matcher matcher = exclusion.path().matcher(path); + if (matcher.matches()) { + return true; + } + } + } + + return false; + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/exclusion/HttpExclusion.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/exclusion/HttpExclusion.java new file mode 100644 index 0000000..7e8e87e --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/auth/exclusion/HttpExclusion.java @@ -0,0 +1,17 @@ +package de.craftsblock.cnet.modules.security.auth.exclusion; + +import de.craftsblock.craftsnet.api.http.HttpMethod; +import de.craftsblock.craftsnet.api.utils.Scheme; +import org.intellij.lang.annotations.RegExp; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.regex.Pattern; + +public record HttpExclusion(Scheme scheme, Pattern path, HashSet methods) implements Exclusion { + + public HttpExclusion(@RegExp String path, HttpMethod... methods) { + this(Scheme.HTTP, Pattern.compile(path), new HashSet<>(Arrays.asList(HttpMethod.normalize(methods)))); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/exclusion/WebSocketExclusion.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/exclusion/WebSocketExclusion.java new file mode 100644 index 0000000..7e8154d --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/auth/exclusion/WebSocketExclusion.java @@ -0,0 +1,14 @@ +package de.craftsblock.cnet.modules.security.auth.exclusion; + +import de.craftsblock.craftsnet.api.utils.Scheme; +import org.intellij.lang.annotations.RegExp; + +import java.util.regex.Pattern; + +public record WebSocketExclusion(Scheme scheme, Pattern path) implements Exclusion { + + public WebSocketExclusion(@RegExp String path) { + this(Scheme.WS, Pattern.compile(path)); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/listener/AuthListener.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/listener/AuthListener.java new file mode 100644 index 0000000..3438ec5 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/auth/listener/AuthListener.java @@ -0,0 +1,40 @@ +package de.craftsblock.cnet.modules.security.auth.listener; + +import de.craftsblock.cnet.modules.security.CraftsNetSecurity; +import de.craftsblock.cnet.modules.security.auth.AuthResult; +import de.craftsblock.cnet.modules.security.auth.event.AuthFailureEvent; +import de.craftsblock.cnet.modules.security.auth.event.AuthSkipEvent; +import de.craftsblock.cnet.modules.security.auth.event.AuthSuccessEvent; +import de.craftsblock.craftscore.event.CancellableEvent; +import de.craftsblock.craftsnet.api.BaseExchange; +import de.craftsblock.craftsnet.events.EventWithCancelReason; + +import java.util.function.BiConsumer; + +sealed interface AuthListener permits PreRequestListener, WebSocketConnectListener { + + CraftsNetSecurity addon(); + + default void authenticate(BaseExchange exchange, CancellableEvent event, T subject, BiConsumer onFailure) { + CraftsNetSecurity addon = this.addon(); + AuthResult result = addon.getAuthChain().authenticate(exchange); + + if (!result.isFailure()) { + addon.getListenerRegistry().call( + result.isOk() + ? new AuthSuccessEvent(exchange, result) + : new AuthSkipEvent(exchange, result) + ); + return; + } + + event.setCancelled(true); + if (event instanceof EventWithCancelReason withCancelReason) { + withCancelReason.setCancelReason("AUTH FAILED"); + } + + addon.getListenerRegistry().call(new AuthFailureEvent(exchange, result)); + onFailure.accept(subject, result); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/listener/PreRequestListener.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/listener/PreRequestListener.java new file mode 100644 index 0000000..416953e --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/auth/listener/PreRequestListener.java @@ -0,0 +1,58 @@ +package de.craftsblock.cnet.modules.security.auth.listener; + +import de.craftsblock.cnet.modules.security.CraftsNetSecurity; +import de.craftsblock.craftscore.event.EventHandler; +import de.craftsblock.craftscore.event.EventPriority; +import de.craftsblock.craftscore.event.ListenerAdapter; +import de.craftsblock.craftscore.json.Json; +import de.craftsblock.craftsnet.CraftsNet; +import de.craftsblock.craftsnet.addon.meta.Startup; +import de.craftsblock.craftsnet.api.http.Exchange; +import de.craftsblock.craftsnet.api.http.Request; +import de.craftsblock.craftsnet.api.http.Response; +import de.craftsblock.craftsnet.autoregister.meta.AutoRegister; +import de.craftsblock.craftsnet.autoregister.meta.constructors.FallbackConstructor; +import de.craftsblock.craftsnet.autoregister.meta.constructors.PreferConstructor; +import de.craftsblock.craftsnet.events.requests.PreRequestEvent; + +@AutoRegister(startup = Startup.LOAD) +public record PreRequestListener(CraftsNet craftsNet, CraftsNetSecurity addon) implements AuthListener, ListenerAdapter { + + @PreferConstructor + public PreRequestListener { + } + + @FallbackConstructor + public PreRequestListener(CraftsNet craftsNet) { + this(craftsNet, CraftsNetSecurity.getInstance()); + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreWhenCancelled = true) + public void handlePreRequestEvent(PreRequestEvent event) { + final Exchange exchange = event.getExchange(); + final Request request = exchange.request(); + + this.authenticate(exchange, event, exchange.response(), ((response, result) -> { + addon.getCraftsNet().getLogger().warning("%s %s from %s \u001b[38;5;9m[%s]".formatted( + request.getHttpMethod(), + request.getRawUrl(), + request.getIp(), + "AUTH FAILED" + )); + + if (!response.headersSent()) { + response.setCode(400); + } + + if (response.sendingFile()) { + return; + } + + response.print(Json.empty() + .set("success", false) + .set("error.code", result.getCode()) + .set("error.message", result.getReason())); + })); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/listener/WebSocketConnectListener.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/listener/WebSocketConnectListener.java new file mode 100644 index 0000000..dc1ea7a --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/auth/listener/WebSocketConnectListener.java @@ -0,0 +1,45 @@ +package de.craftsblock.cnet.modules.security.auth.listener; + +import de.craftsblock.cnet.modules.security.CraftsNetSecurity; +import de.craftsblock.craftscore.buffer.BufferUtil; +import de.craftsblock.craftscore.event.EventHandler; +import de.craftsblock.craftscore.event.EventPriority; +import de.craftsblock.craftscore.event.ListenerAdapter; +import de.craftsblock.craftscore.json.Json; +import de.craftsblock.craftsnet.CraftsNet; +import de.craftsblock.craftsnet.addon.meta.Startup; +import de.craftsblock.craftsnet.api.websocket.ClosureCode; +import de.craftsblock.craftsnet.api.websocket.SocketExchange; +import de.craftsblock.craftsnet.api.websocket.WebSocketClient; +import de.craftsblock.craftsnet.autoregister.meta.AutoRegister; +import de.craftsblock.craftsnet.autoregister.meta.constructors.FallbackConstructor; +import de.craftsblock.craftsnet.autoregister.meta.constructors.PreferConstructor; +import de.craftsblock.craftsnet.events.sockets.ClientConnectEvent; + +@AutoRegister(startup = Startup.LOAD) +public record WebSocketConnectListener(CraftsNet craftsNet, CraftsNetSecurity addon) implements ListenerAdapter, AuthListener { + + @PreferConstructor + public WebSocketConnectListener { + } + + @FallbackConstructor + public WebSocketConnectListener(CraftsNet craftsNet) { + this(craftsNet, CraftsNetSecurity.getInstance()); + } + + @EventHandler(priority = EventPriority.NORMAL, ignoreWhenCancelled = true) + public void handleConnect(ClientConnectEvent event) { + final SocketExchange exchange = event.getExchange(); + this.authenticate( + exchange, event, exchange.client(), + (client, result) -> client.sendMessage( + Json.empty() + .set("success", false) + .set("error.code", result.getCode()) + .set("error.message", result.getReason()) + ) + ); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/token/Token.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/token/Token.java deleted file mode 100644 index e61584b..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/auth/token/Token.java +++ /dev/null @@ -1,89 +0,0 @@ -package de.craftsblock.cnet.modules.security.auth.token; - -import de.craftsblock.cnet.modules.security.utils.Entity; -import de.craftsblock.craftscore.json.Json; -import de.craftsblock.craftscore.utils.id.Snowflake; -import org.jetbrains.annotations.ApiStatus; -import org.springframework.security.crypto.bcrypt.BCrypt; - -import java.util.ArrayList; -import java.util.List; - -/** - * This class represents a token entity that holds information such as - * the token ID, hash, and associated permissions. - * It also provides functionality for validation and serialization. - * - * @param id the unique identifier of the token. - * @param hash the hashed value of the token secret. - * @param permissions a list of {@link TokenPermission}, defining access control rules for the token. - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.3 - * @since 1.0.0-SNAPSHOT - */ -public record Token(long id, String hash, List permissions) implements Entity { - - /** - * Validates if the given secret matches the hashed secret stored in the token. - * - * @param secret the secret to be validated. - * @return {@code true} if the secret matches the hash, {@code false} otherwise. - * @deprecated Use {@link #validate(String)} instead! - */ - @ApiStatus.ScheduledForRemoval(inVersion = "2.0.0") - @Deprecated(since = "1.0.0-pre10", forRemoval = true) - public boolean valid(String secret) { - return this.validate(secret); - } - - /** - * Validates if the given secret matches the hashed secret stored in the token. - * - * @param secret the secret to be validated. - * @return {@code true} if the secret matches the hash, {@code false} otherwise. - */ - public boolean validate(String secret) { - return BCrypt.checkpw(secret, hash()); - } - - /** - * Serializes the {@link Token} object into a {@link Json} object, - * which includes the ID, hash, expiration time, and permission details. - * - * @return a {@link Json} object representing the serialized token. - */ - @Override - public Json serialize() { - Json json = Json.empty(); - json.set("id", id); - json.set("hash", hash); - json.set("permissions", permissions.stream().map(TokenPermission::serialize).map(Json::getObject).toList()); - return json; - } - - /** - * Creates a new {@link Token} object using a hash. - * The token ID is generated using the {@link Snowflake} utility. - * The token will be created with empty permissions by default. - * - * @param hash the hashed token secret. - * @return a new {@link Token} object. - */ - public static Token of(String hash) { - return of(Snowflake.generate(), hash, new ArrayList<>()); - } - - /** - * A private factory method for creating a {@link Token} object with specified - * ID, hash, and permissions. - * - * @param id the unique identifier of the token. - * @param hash the hashed token secret. - * @param permissions a list of {@link TokenPermission} associated with this token. - * @return a new {@link Token} object. - */ - public static Token of(long id, String hash, List permissions) { - return new Token(id, hash, permissions); - } -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/token/TokenManager.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/token/TokenManager.java deleted file mode 100644 index 29e6623..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/auth/token/TokenManager.java +++ /dev/null @@ -1,309 +0,0 @@ -package de.craftsblock.cnet.modules.security.auth.token; - -import de.craftsblock.cnet.modules.security.CNetSecurity; -import de.craftsblock.cnet.modules.security.auth.token.driver.storage.TokenStorageDriver; -import de.craftsblock.cnet.modules.security.events.auth.token.TokenCreateEvent; -import de.craftsblock.cnet.modules.security.events.auth.token.TokenRevokeEvent; -import de.craftsblock.cnet.modules.security.utils.Manager; -import de.craftsblock.craftsnet.api.http.HttpMethod; -import de.craftsblock.craftsnet.utils.PassphraseUtils; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.springframework.security.crypto.bcrypt.BCrypt; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Pattern; - -/** - * Manages a collection of authentication tokens, providing functionality to register, unregister, save, - * and generate tokens with associated permissions. It extends {@link ConcurrentHashMap} to store tokens - * by their unique IDs and implements the {@link Manager} interface for managing token-related operations. - * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.3.3 - * @since 1.0.0-SNAPSHOT - */ -public final class TokenManager extends ConcurrentHashMap implements Manager { - - private static TokenStorageDriver DRIVER; - - private static String TOKEN_PREFIX = "cnet_"; - private static String TOKEN_PREFIX_DELIMITER = "_"; - - /** - * Sets the storage driver to be used for persisting tokens and loads all tokens from it. - *

- * Existing tokens in the manager will be cleared and replaced with the loaded ones. - *

- * - * @param driver The {@link TokenStorageDriver} to be set and used for token persistence. - */ - @ApiStatus.Experimental - public static void setDriver(@NotNull TokenStorageDriver driver) { - TokenManager.DRIVER = driver; - - TokenManager manager = CNetSecurity.getTokenManager(); - if (manager == null) return; - - manager.clear(); - driver.loadAll().forEach(token -> manager.put(token.id(), token)); - } - - /** - * Retrieves the currently set {@link TokenStorageDriver} used for token persistence. - * - * @return The current {@link TokenStorageDriver}, or {@code null} if none is set. - */ - @ApiStatus.Experimental - public static @Nullable TokenStorageDriver getDriver() { - return DRIVER; - } - - /** - * Sets the prefix used when generating token strings. - * - * @param tokenPrefix The prefix string to be used for tokens. - */ - @ApiStatus.Experimental - public static void setTokenPrefix(String tokenPrefix) { - TOKEN_PREFIX = tokenPrefix.replaceAll(TOKEN_PREFIX_DELIMITER + "+", TOKEN_PREFIX_DELIMITER).trim(); - - if (TOKEN_PREFIX.endsWith(TOKEN_PREFIX_DELIMITER)) return; - TOKEN_PREFIX += TOKEN_PREFIX_DELIMITER; - } - - /** - * Retrieves the currently configured token prefix. - * - * @return The token prefix as a string. - */ - @ApiStatus.Experimental - public static String getTokenPrefix() { - return TOKEN_PREFIX; - } - - /** - * Sets the delimiter used to split token components. - *

- * The delimiter will be quoted to ensure it is used correctly in regular expressions. - *

- * - * @param tokenPrefixDelimiter The delimiter to be used in token formatting. - */ - @ApiStatus.Experimental - public static void setTokenPrefixDelimiter(String tokenPrefixDelimiter) { - TOKEN_PREFIX_DELIMITER = Pattern.quote(tokenPrefixDelimiter); - } - - /** - * Retrieves the currently configured token prefix delimiter. - * - * @return The token prefix delimiter as a string. - */ - @ApiStatus.Experimental - public static String getTokenPrefixDelimiter() { - return TOKEN_PREFIX_DELIMITER; - } - - /** - * Registers a new token by adding it to the token manager. - * - * @param token The {@link Token} to be registered. - */ - public void registerToken(Token token) { - try { - TokenCreateEvent event = new TokenCreateEvent(token); - if (event.isCancelled()) { - CNetSecurity.getLogger().debug("Token creation of token " + token.id() + " cancelled!"); - return; - } - - CNetSecurity.callEvent(event); - } catch (InvocationTargetException | IllegalAccessException e) { - throw new RuntimeException(e); - } - - this.put(token.id(), token); - } - - /** - * Unregisters a token by removing it from the token manager. - * - * @param token The {@link Token} to be unregistered. - */ - public void unregisterToken(Token token) { - try { - TokenRevokeEvent event = new TokenRevokeEvent(token); - if (event.isCancelled()) { - CNetSecurity.getLogger().debug("Token revokation for token " + token.id() + " cancelled!"); - return; - } - - CNetSecurity.callEvent(event); - } catch (InvocationTargetException | IllegalAccessException e) { - throw new RuntimeException(e); - } - - this.remove(token.id()); - DRIVER.delete(token); - } - - /** - * Saves the current tokens in the token manager to the driver. - */ - public void save() { - DRIVER.save(this.values()); - } - - /** - * Generates a new token with the provided permissions, creates a random secret, - * hashes the secret using BCrypt, and associates the permissions with the token. - * - * @param permissions An array of {@link TokenPermission} to be associated with the token. - * @return A {@link Map.Entry} containing the plain text secret (as the key) and the generated {@link Token} (as the value). - */ - public Map.Entry generateToken(TokenPermission... permissions) { - return generateToken(Arrays.asList(permissions)); - } - - /** - * Generates a new token with the provided list of permissions, creates a random secret, - * hashes the secret using BCrypt, and associates the permissions with the token. - * - * @param permissions A list of {@link TokenPermission} to be associated with the token. - * @return A {@link Map.Entry} containing the plain text secret (as the key) and the generated {@link Token} (as the value). - */ - public Map.Entry generateToken(List permissions) { - byte[] secret = this.generateTokenSecret(); - String hash = BCrypt.hashpw(secret, BCrypt.gensalt()); - - Token token = Token.of(hash); - token.permissions().addAll(permissions); - registerToken(token); - - Map.Entry tokenEntry = Map.entry(generatePlainToken(token.id(), secret), token); - PassphraseUtils.erase(secret); - return tokenEntry; - } - - /** - * Generates a plain token in the format {@code cnet_[secret]}. - *

- * The token is composed of a UTF-8 prefix, the hexadecimal representation of the ID, and the raw secret bytes. - *

- * - * @param id The identifier to embed in the token, encoded as hexadecimal. - * @param secret The secret byte array to include in the token; must not be null. - * @return A byte array representing the constructed token. - * @throws RuntimeException If an I/O error occurs during token generation. - */ - public byte[] generatePlainToken(long id, byte[] secret) { - try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { - stream.write(TOKEN_PREFIX.getBytes(StandardCharsets.UTF_8)); - stream.write(Long.toHexString(id).getBytes(StandardCharsets.UTF_8)); - stream.write(secret); - - return stream.toByteArray(); - } catch (IOException e) { - throw new RuntimeException("Could not write plain token!", e); - } - } - - /** - * Generates a secure random byte array to be used as a token secret. - *

- * The generated secret is between 45 and 70 bytes long and excludes special characters. - *

- * - * @return A securely generated byte array to be used as a token secret. - */ - public byte[] generateTokenSecret() { - return PassphraseUtils.generateSecure(45, 70, false); - } - - /** - * Retrieves a {@link Token} based on the given token string. - * The token string is expected to contain an identifier in hexadecimal format. - * If the token is invalid or cannot be parsed, this method returns {@code null}. - * - * @param token The token string to be parsed. - * @return The corresponding {@link Token} if found, otherwise {@code null}. - */ - public @Nullable Token getToken(@NotNull String token) { - // Split the token into parts - String[] parts = token.split(TOKEN_PREFIX_DELIMITER); - if (parts.length == 0) return null; - - String part = parts[parts.length - 1]; - if (part.length() < 16) return null; - - try { - long id = Long.parseLong(part.substring(0, 16), 16); - return CNetSecurity.getTokenManager().get(id); - } catch (NumberFormatException | IllegalStateException ignored) { - return null; - } - } - - /** - * Retrieves and validates a {@link Token} for a given request. - * This method first attempts to retrieve the token using {@link #getToken(String)}. - * If the token exists, it verifies the token's validity based on the provided url, domain, http method, and secret. - * - * @param url The requested URL. - * @param domain The domain from which the request originates. - * @param method The HTTP method of the request. - * @param token The token string to be validated. - * @return The validated {@link Token} if authentication is successful, otherwise {@code null}. - */ - public @Nullable Token getValidatedToken(@NotNull String url, @NotNull String domain, @NotNull HttpMethod method, @NotNull String token) { - Token realToken = getToken(token); - if (realToken == null) return null; - - String[] parts = token.split(TOKEN_PREFIX_DELIMITER); - if (parts.length < 2 || parts[1].length() < 16) return null; - - if (!TOKEN_PREFIX.equalsIgnoreCase(parts[0] + TOKEN_PREFIX_DELIMITER)) return null; - - String secret = parts[1].substring(16); - return isTokenValid(url, domain, method, secret, realToken) ? realToken : null; - } - - /** - * Validates whether a given {@link Token} is authorized for the requested action. - * The token is verified using its hashed secret and checked for permission against the specified http method, domain, and url. - * - * @param url The requested URL. - * @param domain The domain from which the request originates. - * @param method The HTTP method of the request. - * @param secret The secret extracted from the token for authentication. - * @param token The {@link Token} object to be validated. - * @return {@code true} if the token is valid and authorized, otherwise {@code false}. - */ - public boolean isTokenValid(@NotNull String url, @NotNull String domain, @NotNull HttpMethod method, @NotNull String secret, Token token) { - if (token == null || secret.isBlank()) return false; - - try { - // Extract the secret from the token and verify it - if (!BCrypt.checkpw(secret, token.hash())) return false; - - // Check the token permissions - return token.permissions().stream() - .anyMatch(permission -> permission.isHttpMethodAllowed(method) - && permission.isDomainAllowed(domain) - && permission.isPathAllowed(url)); - } catch (Exception e) { - throw new RuntimeException("Could not verify the token", e); - } - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/token/TokenPermission.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/token/TokenPermission.java deleted file mode 100644 index 58d5296..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/auth/token/TokenPermission.java +++ /dev/null @@ -1,152 +0,0 @@ -package de.craftsblock.cnet.modules.security.auth.token; - -import de.craftsblock.cnet.modules.security.utils.Entity; -import de.craftsblock.craftscore.json.Json; -import de.craftsblock.craftscore.utils.id.Snowflake; -import de.craftsblock.craftsnet.api.http.HttpMethod; - -import java.util.Arrays; -import java.util.List; -import java.util.regex.Pattern; - -/** - * This class represents a permission model for a token, defining access - * control based on a combination of path patterns, domain patterns, and http methods. - * - * @param path a regular expression pattern representing the allowed path. - * @param domain a regular expression pattern representing the allowed domain. - * @param methods a variable number of {@link HttpMethod} values representing - * the allowed http methods (e.g., GET, POST). - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.1.1 - * @since 1.0.0-SNAPSHOT - */ -public record TokenPermission(long id, String path, String domain, HttpMethod... methods) implements Entity { - - /** - * Checks if a given pattern is a wildcard pattern. - * A pattern is considered a wildcard if it is "*" or ".*". - * - * @param pattern the pattern to check. - * @return {@code true} if the pattern is a wildcard, {@code false} otherwise. - */ - private boolean isWildcard(String pattern) { - return pattern.equals("*") || pattern.equals(".*"); - } - - /** - * Checks if a given value is allowed by matching it against the provided pattern. - * - * @param value the value to be checked (e.g., a path or domain). - * @param pattern the pattern to match against. - * @return {@code true} if the value matches the pattern, {@code false} otherwise. - */ - private boolean isAllowed(String value, String pattern) { - return value.matches(pattern); - } - - /** - * Checks if the path pattern is a wildcard. - * - * @return {@code true} if the path pattern is a wildcard, {@code false} otherwise. - */ - boolean isPathWildcard() { - return isWildcard(path()); - } - - /** - * Determines if a given path is allowed based on the defined path pattern. - * A path is allowed if it either matches the pattern or if the pattern is a wildcard. - * - * @param path the path to check. - * @return {@code true} if the path is allowed, {@code false} otherwise. - */ - boolean isPathAllowed(String path) { - return isPathWildcard() || isAllowed(path, path()); - } - - /** - * Checks if the domain pattern is a wildcard. - * - * @return {@code true} if the domain pattern is a wildcard, {@code false} otherwise. - */ - boolean isDomainWildcard() { - return isWildcard(domain()); - } - - /** - * Determines if a given domain is allowed based on the defined domain pattern. - * A domain is allowed if it either matches the pattern or if the pattern is a wildcard. - * - * @param domain the domain to check. - * @return {@code true} if the domain is allowed, {@code false} otherwise. - */ - boolean isDomainAllowed(String domain) { - return isDomainWildcard() || isAllowed(domain, domain()); - } - - /** - * Determines if a given http method is allowed based on the defined allowed methods. - * - * @param method the http method to check. - * @return {@code true} if the http method is allowed, {@code false} otherwise. - */ - public boolean isHttpMethodAllowed(HttpMethod method) { - List methods = Arrays.asList(methods()); - return methods.contains(HttpMethod.ALL) || methods.contains(HttpMethod.ALL_RAW) || methods.contains(method); - } - - /** - * Serializes the {@link TokenPermission} object into a {@link Json} object. - * The serialization includes the path, domain, and allowed http methods. - * - * @return a {@link Json} object representing the serialized permission details. - */ - @Override - public Json serialize() { - return Json.empty() - .set("id", id()) - .set("path", path()) - .set("domain", domain()) - .set("methods", Arrays.stream(methods()).map(HttpMethod::name).toList()); - } - - /** - * Creates a new {@link TokenPermission} with a given path and http methods. - * The domain pattern defaults to a wildcard (".*"). - * - * @param path the regular expression pattern for the path. - * @param methods the allowed http methods for this permission. - * @return a new {@link TokenPermission} instance. - */ - public static TokenPermission of(String path, HttpMethod... methods) { - return TokenPermission.of(path, ".*", methods); - } - - /** - * Creates a new {@link TokenPermission} with a given path, domain, and http methods. - * - * @param path the regular expression pattern for the path. - * @param domain the regular expression pattern for the domain. - * @param methods the allowed http methods for this permission. - * @return a new {@link TokenPermission} instance. - */ - public static TokenPermission of(String path, String domain, HttpMethod... methods) { - return TokenPermission.of(Snowflake.generate(), path, domain, methods); - } - - /** - * Creates a new {@link TokenPermission} with the specified id, path, domain, and http methods. - * - * @param id the unique id of the permission (usually generated via Snowflake or read from storage). - * @param path the regular expression pattern for the path. - * @param domain the regular expression pattern for the domain. - * @param methods the allowed http methods for this permission. - * @return a new {@link TokenPermission} instance. - */ - public static TokenPermission of(long id, String path, String domain, HttpMethod... methods) { - return new TokenPermission(id, path, domain, methods); - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/token/adapter/TokenAuthAdapter.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/token/adapter/TokenAuthAdapter.java deleted file mode 100644 index b0205cb..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/auth/token/adapter/TokenAuthAdapter.java +++ /dev/null @@ -1,213 +0,0 @@ -package de.craftsblock.cnet.modules.security.auth.token.adapter; - -import de.craftsblock.cnet.modules.security.CNetSecurity; -import de.craftsblock.cnet.modules.security.auth.AuthAdapter; -import de.craftsblock.cnet.modules.security.auth.AuthResult; -import de.craftsblock.cnet.modules.security.auth.token.Token; -import de.craftsblock.cnet.modules.security.auth.token.TokenManager; -import de.craftsblock.cnet.modules.security.events.auth.token.TokenUsedEvent; -import de.craftsblock.craftsnet.api.http.Exchange; -import de.craftsblock.craftsnet.api.http.HttpMethod; -import de.craftsblock.craftsnet.api.http.Request; -import de.craftsblock.craftsnet.api.http.cookies.Cookie; -import de.craftsblock.craftsnet.api.session.Session; -import org.jetbrains.annotations.Nullable; - -import java.util.EnumMap; -import java.util.Map; - -/** - * The {@link TokenAuthAdapter} class implements the {@link AuthAdapter} interface to provide authentication - * functionality using bearer tokens. - *

- * This adapter extracts the token from the Authorization header of a http request, - * validates it, and performs authentication by checking the token's validity - * against the stored tokens managed by the {@link TokenManager}. - * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.5 - * @see TokenAuthType - * @see TokenUsedEvent - * @since 1.0.0-SNAPSHOT - */ -public class TokenAuthAdapter implements AuthAdapter { - - /** - * The expected authorization type for bearer tokens. - */ - public static final String HEADER_AUTH_TYPE = "bearer"; - - private final EnumMap authTypes = new EnumMap<>(TokenAuthType.class); - - private String tokenSessionKey = null; - - /** - * Enables token authentication for the given authentication type using a default name. - *

- * For {@link TokenAuthType#HEADER}, the default name "Authorization" is used. - * For {@link TokenAuthType#COOKIE} and {@link TokenAuthType#SESSION}, no default name is provided and an - * {@link IllegalStateException} is thrown. - *

- * - * @param type The token authentication type to enable. - * @return The current instance of {@code TokenAuthAdapter} for method chaining. - * @throws IllegalStateException if no default name is defined for the given authentication type. - */ - public TokenAuthAdapter enable(TokenAuthType type) { - return switch (type) { - case HEADER -> enable(type, "Authorization"); - case COOKIE, SESSION -> throw new IllegalStateException("No default name for auth type " + type + " found!"); - }; - } - - /** - * Enables token authentication for the given authentication type using the specified name. - * - * @param type The token authentication type to enable. - * @param name The name of the header, cookie, or session attribute to use. - * @return The current instance of {@code TokenAuthAdapter} for method chaining. - */ - public TokenAuthAdapter enable(TokenAuthType type, String name) { - this.authTypes.put(type, name); - return this; - } - - /** - * Disables token authentication for the specified authentication type. - * - * @param type The token authentication type to disable. - * @return The current instance of {@code TokenAuthAdapter} for method chaining. - */ - public TokenAuthAdapter disable(TokenAuthType type) { - this.authTypes.remove(type); - return this; - } - - /** - * Checks if token authentication is enabled for the specified authentication type. - * - * @param type The token authentication type to check. - * @return {@code true} if the authentication type is enabled, {@code false} otherwise. - */ - public boolean isEnabled(TokenAuthType type) { - return this.authTypes.containsKey(type); - } - - /** - * Sets the key where the used token should be stored in the session - * of the exchange. If the session key is {@code null} the token will - * not be stored in the session. - * - * @param sessionKey The key where the token should be stored. - */ - public void setTokenSessionKey(@Nullable String sessionKey) { - this.tokenSessionKey = sessionKey; - } - - /** - * Retrieves the key where the used token is stored inside the session. - * If the token is not stored anywhere in the session this method returns - * {@code null}. - * - * @return The key where the token is stored, or {@code null} when the token - * is not stored in the session. - */ - public @Nullable String getTokenSessionKey() { - return tokenSessionKey; - } - - /** - * Authenticates the user based on the provided token in the request. - *

- * This method checks for the presence of the Authorization header and validates - * the token format. If the token is valid, it retrieves the corresponding - * {@link Token} from the {@link CNetSecurity} and verifies the token's - * secret using BCrypt. If any validation fails, the authentication result is - * marked as failed. - * - * @param result The {@link AuthResult} object where the authentication result will be stored. - * @param exchange The {@link Exchange} object representing the HTTP request. - */ - @Override - public void authenticate(AuthResult result, Exchange exchange) { - if (result.isCancelled()) return; - - if (authTypes.isEmpty()) { - failAuth(result, 501, "No auth type has been set up!"); - return; - } - - for (Map.Entry entry : authTypes.entrySet()) { - TokenAuthType type = entry.getKey(); - String name = entry.getValue(); - if (handle(result, exchange, type, name)) return; - } - - if (result.isCancelled()) return; - failAuth(result, 401, "Requires authentication"); - } - - /** - * Handles the authentication process for a specific token authentication type. - * - * @param result The {@link AuthResult} object to update with authentication status. - * @param exchange The {@link Exchange} object representing the HTTP request and session. - * @param type The token authentication type (e.g., HEADER, COOKIE, SESSION). - * @param name The name of the header, cookie, or session attribute to extract the token from. - * @return {@code true} if the authentication process for this token type has been completed (successfully or not), - * or {@code false} if the token was not found and further processing is required. - */ - private boolean handle(AuthResult result, Exchange exchange, TokenAuthType type, String name) { - if (result.isCancelled()) return true; - - final Request request = exchange.request(); - final Session session = exchange.session(); - - String secret = switch (type) { - case HEADER -> { - // Retrieve the authorization header from the request - String auth_header = request.getHeader(name); - - // Check if the header is present - if (auth_header == null || auth_header.isBlank()) yield null; - - // Split the auth header and check if it has two values and is of the correct type - String[] header = auth_header.split(" "); - if (header.length != 2 || !HEADER_AUTH_TYPE.equalsIgnoreCase(header[0])) { - failAuth(result, 400, "Invalid authorization header!"); - yield null; - } - - // Extract the token from the authorization header - yield header[1]; - } - case COOKIE -> request.getCookies().getOrDefault(name, new Cookie(name, null)).getValue(); - case SESSION -> session.getAsType(name, String.class); - }; - - if (result.isCancelled()) return true; - if (secret == null || secret.isBlank()) return false; - - String url = request.getUrl(); - String domain = request.getDomain(); - HttpMethod method = request.getHttpMethod(); - Token token = CNetSecurity.getTokenManager().getValidatedToken(url, domain, method, secret); - if (token == null) { - failAuth(result, "You do not have access to this ressource!"); - return true; - } - - try { - if (tokenSessionKey != null && !tokenSessionKey.isBlank()) - session.put(tokenSessionKey, token); - - CNetSecurity.callEvent(new TokenUsedEvent(token, type)); - } catch (Exception e) { - failAuth(result, 500, "Failed to verify your token!"); - CNetSecurity.getAddonEntrypoint().logger().error(e, "Failed to verify the api token!"); - } - return true; - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/token/adapter/TokenAuthType.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/token/adapter/TokenAuthType.java deleted file mode 100644 index 2b44a55..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/auth/token/adapter/TokenAuthType.java +++ /dev/null @@ -1,28 +0,0 @@ -package de.craftsblock.cnet.modules.security.auth.token.adapter; - -/** - * Enum representing the supported types of token authentication. - * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @since 1.0.0-SNAPSHOT - */ -public enum TokenAuthType { - - /** - * Token authentication via HTTP header. - */ - HEADER, - - /** - * Token authentication via HTTP cookie. - */ - COOKIE, - - /** - * Token authentication via session attribute. - */ - SESSION, - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/token/driver/storage/FileTokenStorageDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/token/driver/storage/FileTokenStorageDriver.java deleted file mode 100644 index 4bf361d..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/auth/token/driver/storage/FileTokenStorageDriver.java +++ /dev/null @@ -1,164 +0,0 @@ -package de.craftsblock.cnet.modules.security.auth.token.driver.storage; - -import de.craftsblock.cnet.modules.security.CNetSecurity; -import de.craftsblock.cnet.modules.security.auth.token.Token; -import de.craftsblock.cnet.modules.security.auth.token.TokenPermission; -import de.craftsblock.craftscore.json.Json; -import de.craftsblock.craftscore.json.JsonParser; -import de.craftsblock.craftscore.utils.id.Snowflake; -import de.craftsblock.craftsnet.api.http.HttpMethod; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -/** - * A file-based implementation of {@link TokenStorageDriver} that serializes tokens - * and stores them in a json file. - * - *

This implementation is useful for lightweight deployments where a database - * is not available or necessary.

- * - *

The file is synchronized during read/write operations to ensure thread safety.

- * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.1 - * @see Json - * @see TokenStorageDriver - * @since 1.0.0-SNAPSHOT - */ -public class FileTokenStorageDriver extends TokenStorageDriver { - - private final Path saveFile; - - /** - * Constructs a {@link FileTokenStorageDriver} that stores tokens in the default {@code tokens.json} - * file within the plugin's data folder. - */ - public FileTokenStorageDriver() { - this(CNetSecurity.getAddonEntrypoint().getDataFolder().toPath().resolve("tokens.json")); - } - - /** - * Constructs a {@link FileTokenStorageDriver} that stores tokens in the specified file. - * - * @param saveFile The path to the file where tokens will be stored. - */ - public FileTokenStorageDriver(Path saveFile) { - if (!Files.exists(saveFile)) { - try { - Files.createFile(saveFile); - } catch (IOException e) { - throw new RuntimeException("Could not create save file at %s!".formatted(saveFile.toAbsolutePath().toString()), e); - } - } - - if (!Files.isRegularFile(saveFile) && !Files.isSymbolicLink(saveFile)) - throw new IllegalArgumentException(""); - - this.saveFile = saveFile; - } - - /** - * Saves the given collection of tokens to the configured json file. - *

- * Each token is serialized to json and stored under its id as the key. - * The write operation is synchronized to avoid concurrent access issues. - * - * @param tokens The tokens to save. - */ - @Override - public void save(Collection tokens) { - Json json = Json.empty(); - tokens.forEach(token -> json.set(String.valueOf(token.id()), token.serialize())); - - synchronizedSave(json); - } - - /** - * Loads all tokens from the json file. - *

- * Parses each json object and reconstructs the {@link Token} and associated {@link TokenPermission}s. - * The read operation is synchronized to ensure thread safety. - * - * @return A collection of all loaded tokens, or an empty list if the file is empty or invalid. - */ - @Override - public Collection loadAll() { - Json json = synchronizedRead(); - if (!json.getObject().isJsonObject()) return List.of(); - - return json.values().stream() - .map(JsonParser::parse) - .map(this::createTokenFromJson) - .toList(); - } - - /** - * Synchronously reads the contents of the json file and parses it into a {@link Json} object. - * - * @return The parsed {@link Json} object from the file. - */ - private Json synchronizedRead() { - synchronized (saveFile) { - return JsonParser.parse(saveFile); - } - } - - /** - * Synchronously writes the given {@link Json} object to the file. - * - * @param json The {@link Json} data to write to the file. - */ - private void synchronizedSave(Json json) { - synchronized (saveFile) { - json.save(saveFile); - } - } - - /** - * Returns the file path used for saving and loading token data. - * - * @return The path to the save file. - */ - public Path getSaveFile() { - return saveFile; - } - - /** - * Constructs a {@link Token} from the given json object. - *

- * Parses the token id, hash, and all associated permissions from the nested json structure. - * - * @param json The json object representing a token. - * @return The constructed {@link Token}. - */ - private Token createTokenFromJson(Json json) { - return Token.of(json.getLong("id"), json.getString("hash"), - new ArrayList<>(json.getJsonList("permissions").stream().map(this::createTokenPermissionFromJson).toList())); - } - - /** - * Constructs a {@link TokenPermission} from the given json object. - *

- * If the permission does not contain an "id" field, a new id is generated using {@link Snowflake} - * to maintain compatibility with older formats. - * - * @param json The json object representing a permission. - * @return The constructed {@link TokenPermission}. - */ - private TokenPermission createTokenPermissionFromJson(Json json) { - // Required for backwards compatibility, as the old token permissions do not have an id - long id = json.contains("id") ? json.getLong("id") : Snowflake.generate(); - - return TokenPermission.of( - id, json.getString("path"), json.getString("domain"), - json.getStringList("methods").stream().map(HttpMethod::parse).toArray(HttpMethod[]::new) - ); - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/token/driver/storage/SQLTokenStorageDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/token/driver/storage/SQLTokenStorageDriver.java deleted file mode 100644 index 6642419..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/auth/token/driver/storage/SQLTokenStorageDriver.java +++ /dev/null @@ -1,298 +0,0 @@ -package de.craftsblock.cnet.modules.security.auth.token.driver.storage; - -import de.craftsblock.cnet.modules.security.auth.token.Token; -import de.craftsblock.cnet.modules.security.auth.token.TokenPermission; -import de.craftsblock.craftscore.sql.SQL; -import de.craftsblock.craftsnet.api.http.HttpMethod; - -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.*; - -/** - * A concrete implementation of {@link TokenStorageDriver} that persists and retrieves tokens - * and their associated permissions using an SQL-based relational database. - * - *

This class creates the required database tables, views, and triggers if they do not already exist.

- *

It supports saving, deleting, and loading tokens, including managing the many-to-many relationship - * between tokens and permissions.

- * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.1 - * @see SQL - * @see TokenStorageDriver - * @since 1.0.0-SNAPSHOT - */ -public class SQLTokenStorageDriver extends TokenStorageDriver { - - private final SQL sql; - - /** - * Constructs a new {@link SQLTokenStorageDriver} with the given {@link SQL} connection. - * - * @param sql An active {@link SQL} connection to a relational database. - * @throws IllegalStateException If the SQL connection is not active. - */ - public SQLTokenStorageDriver(SQL sql) { - this.sql = sql; - - try { - if (!this.sql.isConnected()) - throw new IllegalStateException("The sql instance must be connected to the database!"); - } catch (SQLException e) { - throw new RuntimeException("Could not verify sql connection status!", e); - } - - // Create tables if they not exists - this.createTables(); - } - - /** - * Saves a collection of tokens to the database by calling {@link #save(Token)} for each token. - * - * @param tokens The collection of {@link Token} instances to persist. - */ - @Override - public void save(Collection tokens) { - tokens.forEach(this::save); - } - - /** - * Saves a single token and its associated permissions to the database. - *

- * This involves: - *

    - *
  • Inserting or updating the token in the {@code cnet_security_tokens} table
  • - *
  • Inserting or updating each permission in the {@code cnet_security_permissions} table
  • - *
  • Linking the token to its permissions in the {@code cnet_security_token_permissions} table
  • - *
- * - * @param token The {@link Token} to persist. - */ - public void save(Token token) { - try (PreparedStatement statement = this.sql.prepareStatement( - "INSERT INTO `cnet_security_tokens` (`id`, `hash`) VALUES (?,?) ON DUPLICATE KEY UPDATE `hash`=?;" - )) { - statement.setLong(1, token.id()); - statement.setString(2, token.hash()); - statement.setString(3, token.hash()); - - this.sql.update(statement); - } catch (SQLException e) { - throw new RuntimeException("Could not save token %s to the database!".formatted(token.id()), e); - } - - List permissionIDs = new ArrayList<>(); - token.permissions().forEach(permission -> { - try (PreparedStatement statement = this.sql.prepareStatement( - "INSERT INTO `cnet_security_permissions` (`id`, `path`, `domain`, `http_methods`) VALUES (?, ?, ?, ?) " + - "ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id);", true - )) { - statement.setLong(1, permission.id()); - statement.setString(2, permission.path()); - statement.setString(3, permission.domain()); - statement.setString(4, HttpMethod.join(permission.methods())); - - statement.executeUpdate(); - - try (ResultSet keys = statement.getGeneratedKeys()) { - if (keys.next()) - permissionIDs.add(keys.getLong(1)); - else permissionIDs.add(permission.id()); - } - } catch (SQLException e) { - throw new RuntimeException("Could not create token permission for token %s!".formatted(token.id()), e); - } - }); - - permissionIDs.forEach(id -> { - try (PreparedStatement statement = this.sql.prepareStatement( - "INSERT IGNORE INTO `cnet_security_token_permissions` (`token`, `permission`) VALUES (?,?);" - )) { - statement.setLong(1, token.id()); - statement.setLong(2, id); - - this.sql.update(statement); - } catch (SQLException e) { - throw new RuntimeException("Could not link token permission %s with token %s!".formatted(id, token.id()), e); - } - }); - } - - /** - * Deletes a token with the specified id from the database. - *

- * Related entries in the {@code cnet_security_token_permissions} table will also be removed, - * and a cleanup trigger may delete unused permissions. - * - * @param id The ID of the token to delete. - */ - @Override - public void delete(long id) { - try (PreparedStatement statement = this.sql.prepareStatement( - "DELETE FROM `cnet_security_token_permissions` WHERE `cnet_security_token_permissions`.`token`=?;" - )) { - statement.setLong(1, id); - this.sql.update(statement); - } catch (SQLException e) { - throw new RuntimeException("Could not delete token permissions for token %s from the database!".formatted(id), e); - } - - try (PreparedStatement statement = this.sql.prepareStatement( - "DELETE FROM `cnet_security_tokens` WHERE `cnet_security_tokens`.`id`=?;" - )) { - statement.setLong(1, id); - this.sql.update(statement); - } catch (SQLException e) { - throw new RuntimeException("Could not delete token %s from the database!".formatted(id), e); - } - } - - /** - * Loads all tokens and their associated permissions from the database. - * - * @return A collection of {@link Token} instances with their full permission sets. - */ - @Override - public Collection loadAll() { - try (ResultSet result = this.sql.query("SELECT * FROM `cnet_security_tokens_merged`;")) { - return createTokensFromResultSet(result).values(); - } catch (SQLException e) { - throw new RuntimeException("Could not load all tokens in the database!", e); - } - } - - /** - * Constructs tokens and their permissions from the result set of the merged view. - * - * @param result The {@link ResultSet} containing joined token and permission data. - * @return A map of token ID to {@link Token} instance. - */ - private Map createTokensFromResultSet(ResultSet result) { - Map tokens = new HashMap<>(); - - try { - while (result.next()) { - long id = result.getLong("token_id"); - String hash = result.getString("hash"); - - Token token = tokens.computeIfAbsent(id, tokenID -> Token.of(tokenID, hash, new ArrayList<>())); - token.permissions().add(createTokenPermissionFromResultSet(result)); - } - } catch (SQLException e) { - throw new RuntimeException("Could not read token from database!", e); - } - - return tokens; - } - - /** - * Creates a {@link TokenPermission} object from the current row of the result set. - * - * @param result The {@link ResultSet} to extract permission data from. - * @return The constructed {@link TokenPermission}. - */ - private TokenPermission createTokenPermissionFromResultSet(ResultSet result) { - try { - HttpMethod[] methods = Arrays.stream(result.getString("http_methods").split("\\|")) - .map(HttpMethod::parse) - .toArray(HttpMethod[]::new); - - return TokenPermission.of(result.getLong("permission_id"), - result.getString("path"), result.getString("domain"), - methods - ); - } catch (SQLException e) { - throw new RuntimeException("Could not read token permission from database!", e); - } - } - - /** - * Initializes the database schema including required tables, views, and triggers - * for managing tokens and their permissions. - */ - private void createTables() { - this.sqlCreate("table cnet_security_tokens", """ - CREATE TABLE IF NOT EXISTS `cnet_security_tokens` ( - `id` BIGINT NOT NULL , - `hash` VARCHAR(128) NOT NULL , - `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP , - `updated_at` TIMESTAMP on update CURRENT_TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP , - PRIMARY KEY (`id`) - ); - """); - - this.sqlCreate("table cnet_security_permissions", """ - CREATE TABLE IF NOT EXISTS `cnet_security_permissions` ( - `id` BIGINT NOT NULL , - `path` VARCHAR(256) NOT NULL , - `domain` VARCHAR(256) NOT NULL , - `http_methods` VARCHAR(128) NOT NULL , - `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP , - `updated_at` TIMESTAMP on update CURRENT_TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP , - PRIMARY KEY (`id`) , - UNIQUE KEY `unique_permission` (`path`, `domain`, `http_methods`) - ); - """); - - this.sqlCreate("table cnet_security_token_permissions", """ - CREATE TABLE IF NOT EXISTS `cnet_security_token_permissions` ( - `token` BIGINT NOT NULL , - `permission` BIGINT NOT NULL , - `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP , - `updated_at` TIMESTAMP on update CURRENT_TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP , - PRIMARY KEY(`token`, `permission`) , - FOREIGN KEY (`token`) REFERENCES `cnet_security_tokens`(`id`) ON DELETE CASCADE ON UPDATE CASCADE , - FOREIGN KEY (`permission`) REFERENCES `cnet_security_permissions`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE - ); - """); - - this.sqlCreate("view cnet_security_tokens_merged", """ - CREATE OR REPLACE VIEW `cnet_security_tokens_merged` AS SELECT - `cnet_security_tokens`.`id` AS `token_id` , - `cnet_security_tokens`.`hash` , - `cnet_security_permissions`.`id` AS `permission_id` , - `cnet_security_permissions`.`path` , - `cnet_security_permissions`.`domain` , - `cnet_security_permissions`.`http_methods` - FROM `cnet_security_tokens` - JOIN `cnet_security_token_permissions` ON (`cnet_security_tokens`.`id` = `cnet_security_token_permissions`.`token`) - JOIN `cnet_security_permissions` ON (`cnet_security_token_permissions`.`permission` = `cnet_security_permissions`.`id`) - """); - - this.sqlCreate("trigger cnet_security_cleanup_unused_permissions", """ - CREATE OR REPLACE TRIGGER `cnet_security_cleanup_unused_permissions` - AFTER DELETE ON `cnet_security_token_permissions` - FOR EACH ROW - BEGIN - DECLARE remaining INT; - \s - SELECT COUNT(*) INTO remaining - FROM `cnet_security_token_permissions` - WHERE `cnet_security_token_permissions`.`permission` = OLD.`permission`; - \s - IF remaining = 0 THEN - DELETE FROM `cnet_security_permissions` - WHERE `cnet_security_permissions`.`id` = OLD.`permission`; - END IF; - END; - """); - } - - /** - * Executes an SQL update for the given schema object creation command. - * - * @param target A descriptive name for the target being created. - * @param sqlCommand The SQL DDL command to execute. - */ - private void sqlCreate(String target, String sqlCommand) { - try { - this.sql.update(sqlCommand); - } catch (SQLException e) { - throw new RuntimeException("Could not create %s!".formatted(target), e); - } - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/token/driver/storage/TokenStorageDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/token/driver/storage/TokenStorageDriver.java deleted file mode 100644 index b06ca39..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/auth/token/driver/storage/TokenStorageDriver.java +++ /dev/null @@ -1,52 +0,0 @@ -package de.craftsblock.cnet.modules.security.auth.token.driver.storage; - -import de.craftsblock.cnet.modules.security.auth.token.Token; - -import java.util.Collection; - -/** - * Abstract base class representing a storage driver for authentication tokens. - * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @see Token - * @since 1.0.0-SNAPSHOT - */ -public abstract class TokenStorageDriver { - - /** - * Persists the given collection of tokens to the underlying storage mechanism. - * - * @param tokens A collection of {@link Token} instances to be saved. - */ - public abstract void save(Collection tokens); - - /** - * Loads all tokens currently stored in the underlying storage. - * - * @return A collection of all {@link Token} instances retrieved from storage. - */ - public abstract Collection loadAll(); - - /** - * Deletes the specified token from the storage. - *

- * This is a convenience method that delegates to {@link #delete(long)} using the tokens id. - *

- * - * @param token The {@link Token} instance to be deleted. - */ - public void delete(Token token) { - this.delete(token.id()); - } - - /** - * Deletes a token identified by its unique id. - * - * @param id the unique identifier of the token to delete. - */ - public void delete(long id) { - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/events/auth/AuthFailedEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/events/auth/AuthFailedEvent.java deleted file mode 100644 index eb78712..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/events/auth/AuthFailedEvent.java +++ /dev/null @@ -1,35 +0,0 @@ -package de.craftsblock.cnet.modules.security.events.auth; - -import de.craftsblock.craftsnet.api.http.Exchange; -import org.jetbrains.annotations.NotNull; - -/** - * Represents an event triggered when authentication fails. - *

- * This event extends {@link GenericAuthResultEvent} to provide - * information about the failed authentication attempt, such as the - * associated {@link Exchange}. - *

- * - *

Listeners can use this event to handle authentication failures, - * such as logging the attempt or displaying an additional error message.

- * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @since 1.0.0-SNAPSHOT - */ -public class AuthFailedEvent extends GenericAuthResultEvent { - - /** - * Constructs a new {@link AuthFailedEvent}. - * - * @param exchange The HTTP exchange associated with the failed authentication. - * Must not be null. - * @throws NullPointerException If {@code exchange} is null. - */ - public AuthFailedEvent(@NotNull Exchange exchange) { - super(exchange); - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/events/auth/AuthSuccessEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/events/auth/AuthSuccessEvent.java deleted file mode 100644 index 558866c..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/events/auth/AuthSuccessEvent.java +++ /dev/null @@ -1,35 +0,0 @@ -package de.craftsblock.cnet.modules.security.events.auth; - -import de.craftsblock.craftsnet.api.http.Exchange; -import org.jetbrains.annotations.NotNull; - -/** - * Represents an event triggered when authentication is successful. - *

- * This event extends {@link GenericAuthResultEvent} to provide - * information about the successful authentication, such as the - * associated {@link Exchange}. - *

- * - *

Listeners can use this event to perform post-authentication actions, - * such as logging or granting access to specific resources.

- * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @since 1.0.0-SNAPSHOT - */ -public class AuthSuccessEvent extends GenericAuthResultEvent { - - /** - * Constructs a new {@link AuthSuccessEvent}. - * - * @param exchange The HTTP exchange associated with the successful authentication. - * Must not be null. - * @throws NullPointerException If {@code exchange} is null. - */ - public AuthSuccessEvent(@NotNull Exchange exchange) { - super(exchange); - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/events/auth/GenericAuthEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/events/auth/GenericAuthEvent.java deleted file mode 100644 index c8fa72c..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/events/auth/GenericAuthEvent.java +++ /dev/null @@ -1,15 +0,0 @@ -package de.craftsblock.cnet.modules.security.events.auth; - -import de.craftsblock.craftscore.event.Event; - -/** - * Represents a generic base class for all authentication related events. - * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @see Event - * @since 1.0.0-SNAPSHOT - */ -public abstract class GenericAuthEvent extends Event { -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/events/auth/GenericAuthResultEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/events/auth/GenericAuthResultEvent.java deleted file mode 100644 index 00d88f2..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/events/auth/GenericAuthResultEvent.java +++ /dev/null @@ -1,73 +0,0 @@ -package de.craftsblock.cnet.modules.security.events.auth; - -import de.craftsblock.craftsnet.api.http.Exchange; -import de.craftsblock.craftsnet.api.http.Request; -import de.craftsblock.craftsnet.api.http.Response; -import de.craftsblock.craftsnet.api.session.Session; -import org.jetbrains.annotations.NotNull; - -/** - * Represents a base class for authentication related events that involve - * an HTTP {@link Exchange}. This class provides access to the request, - * response, and session storage associated with the exchange. - * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @see Exchange - * @see Request - * @see Response - * @see Session - * @since 1.0.0-SNAPSHOT - */ -public abstract class GenericAuthResultEvent extends GenericAuthEvent { - - private final @NotNull Exchange exchange; - - /** - * Constructs a new {@link GenericAuthResultEvent}. - * - * @param exchange The HTTP exchange associated with this event. Must not be null. - * @throws NullPointerException If {@code exchange} is null. - */ - public GenericAuthResultEvent(@NotNull Exchange exchange) { - this.exchange = exchange; - } - - /** - * Gets the HTTP exchange associated with this event. - * - * @return The associated {@link Exchange}. - */ - public @NotNull Exchange getExchange() { - return exchange; - } - - /** - * Gets the HTTP request associated with this event. - * - * @return The associated {@link Request}. - */ - public Request getRequest() { - return exchange.request(); - } - - /** - * Gets the HTTP response associated with this event. - * - * @return The associated {@link Response}. - */ - public Response getResponse() { - return exchange.response(); - } - - /** - * Gets the session storage associated with this event. - * - * @return The associated {@link Session}. - */ - public Session getStorage() { - return exchange.session(); - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/CancellableTokenEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/CancellableTokenEvent.java deleted file mode 100644 index eb919ae..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/CancellableTokenEvent.java +++ /dev/null @@ -1,56 +0,0 @@ -package de.craftsblock.cnet.modules.security.events.auth.token; - -import de.craftsblock.cnet.modules.security.auth.token.Token; -import de.craftsblock.craftscore.event.Cancellable; -import org.jetbrains.annotations.NotNull; - - -/** - * Represents a cancellable token-related event. - *

- * This class extends {@link GenericTokenEvent} and implements {@link Cancellable}, - * allowing the event to be cancelled during processing. - *

- * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @see GenericTokenEvent - * @see Cancellable - * @since 1.0.0-SNAPSHOT - */ -public abstract class CancellableTokenEvent extends GenericTokenEvent implements Cancellable { - - private boolean cancelled = false; - - /** - * Constructs a new {@code CancellableTokenEvent}. - * - * @param token The token associated with this event. Must not be null. - * @throws NullPointerException If {@code token} is null. - */ - public CancellableTokenEvent(@NotNull Token token) { - super(token); - } - - /** - * Sets the cancellation state of this event. - * - * @param cancelled {@code true} to cancel the event, {@code false} to allow it to proceed. - */ - @Override - public void setCancelled(boolean cancelled) { - this.cancelled = cancelled; - } - - /** - * Checks whether this event has been cancelled. - * - * @return {@code true} if the event is cancelled, {@code false} otherwise. - */ - @Override - public boolean isCancelled() { - return cancelled; - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/GenericTokenEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/GenericTokenEvent.java deleted file mode 100644 index 41a4e38..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/GenericTokenEvent.java +++ /dev/null @@ -1,44 +0,0 @@ -package de.craftsblock.cnet.modules.security.events.auth.token; - -import de.craftsblock.cnet.modules.security.auth.token.Token; -import de.craftsblock.cnet.modules.security.events.auth.GenericAuthEvent; -import org.jetbrains.annotations.NotNull; - -/** - * Represents a generic event related to authentication tokens. - *

- * This class serves as a base for more specific token related events - * and provides access to the associated {@link Token}. - *

- * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @see GenericAuthEvent - * @see Token - * @since 1.0.0-SNAPSHOT - */ -public abstract class GenericTokenEvent extends GenericAuthEvent { - - private final @NotNull Token token; - - /** - * Constructs a new {@link GenericTokenEvent}. - * - * @param token The token associated with this event. Must not be null. - * @throws NullPointerException If {@code token} is null. - */ - public GenericTokenEvent(@NotNull Token token) { - this.token = token; - } - - /** - * Returns the token associated with this event. - * - * @return The associated {@link Token}, never null. - */ - public @NotNull Token getToken() { - return token; - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/TokenCreateEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/TokenCreateEvent.java deleted file mode 100644 index 4c599c1..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/TokenCreateEvent.java +++ /dev/null @@ -1,33 +0,0 @@ -package de.craftsblock.cnet.modules.security.events.auth.token; - -import de.craftsblock.cnet.modules.security.auth.token.Token; -import org.jetbrains.annotations.NotNull; - -/** - * Event triggered before a new token is created. - *

- * This event extends {@link CancellableTokenEvent}, allowing listeners to cancel the token creation process - * if necessary. Cancellation might be useful in cases where certain conditions for token creation - * are not met. - *

- * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @see CancellableTokenEvent - * @see Token - * @since 1.0.0-SNAPSHOT - */ -public class TokenCreateEvent extends CancellableTokenEvent { - - /** - * Constructs a new {@link TokenCreateEvent}. - * - * @param token The token being created. Must not be null. - * @throws NullPointerException If {@code token} is null. - */ - public TokenCreateEvent(@NotNull Token token) { - super(token); - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/TokenRevokeEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/TokenRevokeEvent.java deleted file mode 100644 index e484a87..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/TokenRevokeEvent.java +++ /dev/null @@ -1,33 +0,0 @@ -package de.craftsblock.cnet.modules.security.events.auth.token; - -import de.craftsblock.cnet.modules.security.auth.token.Token; -import org.jetbrains.annotations.NotNull; - -/** - * Event triggered before a token is revoked. - *

- * This event extends {@link CancellableTokenEvent}, allowing listeners to cancel the revocation process - * if necessary. For example, cancellation might occur if the revocation request does not meet certain conditions - * or is unauthorized. - *

- * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @see CancellableTokenEvent - * @see Token - * @since 1.0.0-SNAPSHOT - */ -public class TokenRevokeEvent extends CancellableTokenEvent { - - /** - * Constructs a new {@link TokenRevokeEvent}. - * - * @param token The token being revoked. Must not be null. - * @throws NullPointerException If {@code token} is null. - */ - public TokenRevokeEvent(@NotNull Token token) { - super(token); - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/TokenUsedEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/TokenUsedEvent.java deleted file mode 100644 index 66d7ab4..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/events/auth/token/TokenUsedEvent.java +++ /dev/null @@ -1,43 +0,0 @@ -package de.craftsblock.cnet.modules.security.events.auth.token; - -import de.craftsblock.cnet.modules.security.auth.token.Token; -import de.craftsblock.cnet.modules.security.auth.token.adapter.TokenAuthType; -import org.jetbrains.annotations.NotNull; - -/** - * Event triggered when a token is successfully used. - * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.1 - * @see GenericTokenEvent - * @see Token - * @since 1.0.0-SNAPSHOT - */ -public class TokenUsedEvent extends GenericTokenEvent { - - private final TokenAuthType type; - - /** - * Constructs a new {@link TokenUsedEvent}. - * - * @param token The {@link Token} that has been used. Must not be null. - * @param type The {@link TokenAuthType} where the {@link Token} was found. Must not be null. - * @throws NullPointerException If {@code token} is null. - */ - public TokenUsedEvent(@NotNull Token token, @NotNull TokenAuthType type) { - super(token); - this.type = type; - } - - /** - * Retrieves the {@link TokenAuthType} where the {@link Token} was - * found. - * - * @return The {@link TokenAuthType} where the {@link Token} was found. - */ - public @NotNull TokenAuthType getAuthType() { - return type; - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/events/ratelimit/GenericRateLimitEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/events/ratelimit/GenericRateLimitEvent.java deleted file mode 100644 index 7235820..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/events/ratelimit/GenericRateLimitEvent.java +++ /dev/null @@ -1,27 +0,0 @@ -package de.craftsblock.cnet.modules.security.events.ratelimit; - -import de.craftsblock.craftscore.event.Event; - -/** - * The {@link GenericRateLimitEvent} serves as a base class for all rate-limiting-related events. - *

- * Subclasses of this event can be used to handle various rate-limiting scenarios, - * such as when a rate limit is exceeded or reset. - *

- * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @see Event - * @since 1.0.0-SNAPSHOT - */ -public abstract class GenericRateLimitEvent extends Event { - - /** - * Constructs a new {@link GenericRateLimitEvent}. - */ - public GenericRateLimitEvent() { - - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/events/ratelimit/RateLimitExceededEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/events/ratelimit/RateLimitExceededEvent.java deleted file mode 100644 index 9c17ff0..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/events/ratelimit/RateLimitExceededEvent.java +++ /dev/null @@ -1,63 +0,0 @@ -package de.craftsblock.cnet.modules.security.events.ratelimit; - -import de.craftsblock.cnet.modules.security.ratelimit.RateLimitAdapter; -import de.craftsblock.craftsnet.api.http.Exchange; - -import java.util.List; - -/** - * The {@link RateLimitExceededEvent} is triggered when one or more rate limits are exceeded for a specific HTTP request. - * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @see GenericRateLimitEvent - * @see RateLimitAdapter - * @see Exchange - * @since 1.0.0-SNAPSHOT - */ -public class RateLimitExceededEvent extends GenericRateLimitEvent { - - private final Exchange exchange; - private final List exceeded; - - /** - * Constructs a new {@link RateLimitExceededEvent} with a given {@link Exchange} and a variable number of {@link RateLimitAdapter}s. - * - * @param exchange The {@link Exchange} representing the HTTP request that caused the rate limit to be exceeded. - * @param exceeded A variable number of {@link RateLimitAdapter}s responsible for exceeding the rate limits. - */ - public RateLimitExceededEvent(Exchange exchange, RateLimitAdapter... exceeded) { - this(exchange, List.of(exceeded)); - } - - /** - * Constructs a new {@link RateLimitExceededEvent} with a given {@link Exchange} and a list of {@link RateLimitAdapter}s. - * - * @param exchange The {@link Exchange} representing the HTTP request that caused the rate limit to be exceeded. - * @param exceeded A list of {@link RateLimitAdapter}s responsible for exceeding the rate limits. - */ - public RateLimitExceededEvent(Exchange exchange, List exceeded) { - this.exchange = exchange; - this.exceeded = exceeded; - } - - /** - * Gets the {@link Exchange} associated with this event. - * - * @return The {@link Exchange} associated with the rate limiting event. - */ - public Exchange getExchange() { - return exchange; - } - - /** - * Gets the list of {@link RateLimitAdapter}s responsible for the rate limit being exceeded. - * - * @return A list of {@link RateLimitAdapter}s that exceeded their limits. - */ - public List getExceeded() { - return exceeded; - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/listeners/PreRequestListener.java b/src/main/java/de/craftsblock/cnet/modules/security/listeners/PreRequestListener.java deleted file mode 100644 index 5e0a617..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/listeners/PreRequestListener.java +++ /dev/null @@ -1,141 +0,0 @@ -package de.craftsblock.cnet.modules.security.listeners; - -import de.craftsblock.cnet.modules.security.CNetSecurity; -import de.craftsblock.cnet.modules.security.auth.AuthResult; -import de.craftsblock.cnet.modules.security.auth.chains.AuthChain; -import de.craftsblock.cnet.modules.security.events.auth.AuthFailedEvent; -import de.craftsblock.cnet.modules.security.events.auth.AuthSuccessEvent; -import de.craftsblock.cnet.modules.security.events.auth.GenericAuthResultEvent; -import de.craftsblock.craftscore.event.EventHandler; -import de.craftsblock.craftscore.event.EventPriority; -import de.craftsblock.craftscore.event.ListenerAdapter; -import de.craftsblock.craftscore.json.Json; -import de.craftsblock.craftsnet.CraftsNet; -import de.craftsblock.craftsnet.addon.meta.Startup; -import de.craftsblock.craftsnet.api.http.Exchange; -import de.craftsblock.craftsnet.api.http.Request; -import de.craftsblock.craftsnet.api.http.Response; -import de.craftsblock.craftsnet.autoregister.meta.AutoRegister; -import de.craftsblock.craftsnet.events.EventWithCancelReason; -import de.craftsblock.craftsnet.events.requests.PreRequestEvent; -import de.craftsblock.craftsnet.events.requests.routes.RouteRequestEvent; -import de.craftsblock.craftsnet.events.requests.shares.ShareRequestEvent; - -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; - -/** - * The PreRequestListener class listens for pre-request events and processes - * authentication chains to determine if an incoming request should be allowed. - * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.1.2 - * @since 1.0.0-SNAPSHOT - */ -@AutoRegister(startup = Startup.LOAD) -public class PreRequestListener implements ListenerAdapter { - - private final CraftsNet craftsNet; - - /** - * Constructs a new {@link PreRequestEvent}. - * - * @param craftsNet The {@link CraftsNet} instance bound to this {@link ListenerAdapter}. - */ - public PreRequestListener(CraftsNet craftsNet) { - this.craftsNet = craftsNet; - } - - /** - * Handles the {@link PreRequestEvent}. This method is triggered when a pre-request - * event occurs and processes the authentication chains. - * - * @param event The {@link PreRequestEvent} containing information about the request. - * @throws InvocationTargetException If an error occurs while calling / processing the event system - * @throws IllegalAccessException If an error occurs while calling / processing the event system - */ - @EventHandler - public void handleAuthChains(PreRequestEvent event) throws InvocationTargetException, IllegalAccessException { - if (event.isCancelled()) return; - - Exchange exchange = event.getExchange(); - final Request request = exchange.request(); - - // Iterate through each authentication chain - for (AuthChain chain : CNetSecurity.getAuthChainManager()) { - // Authenticate the incoming request using the current chain - AuthResult result = chain.authenticate(exchange); - - // Continue if the authentication was cancelled - if (!result.isCancelled()) continue; - - event.setCancelled(true); // Cancel the event - AuthFailedEvent authFailedEvent = new AuthFailedEvent(exchange); - - // Send an error response back to the client - Response response = exchange.response(); - if (!response.headersSent()) response.setCode(result.getCode()); - response.print(Json.empty().set("status", String.valueOf(result.getCode())) - .set("message", result.getCancelReason())); - - craftsNet.logger().debug("%s %s from %s \u001b[38;5;9m[%s]".formatted( - request.getHttpMethod(), - request.getRawUrl(), - request.getIp(), - "AUTH FAILED" - )); - - CNetSecurity.callEvent(authFailedEvent); - - return; - } - - AuthSuccessEvent authSuccessEvent = new AuthSuccessEvent(exchange); - CNetSecurity.callEvent(authSuccessEvent); - } - - /** - * Handles the {@link RouteRequestEvent}. This method is triggered when a route request - * event occurs and processes the rate limit chain. - * - * @param event The {@link RouteRequestEvent} containing information about the request. - */ - @EventHandler(priority = EventPriority.HIGH) - public void handleRateLimiter(RouteRequestEvent event) { - handleRateLimiter(event, event.getExchange()); - } - - /** - * Handles the {@link ShareRequestEvent}. This method is triggered when a share request - * event occurs and processes the rate limit chain. - * - * @param event The {@link ShareRequestEvent} containing information about the share request. - */ - @EventHandler(priority = EventPriority.HIGH) - public void handleRateLimiter(ShareRequestEvent event) { - handleRateLimiter(event, event.getExchange()); - } - - /** - * Processes the rate limit chain. - * - * @param event The {@link EventWithCancelReason} that was fired. - * @param exchange The {@link Exchange} containing information about the request. - */ - public void handleRateLimiter(EventWithCancelReason event, Exchange exchange) { - if (CNetSecurity.getRateLimitManager().isRateLimited(exchange)) { - // Cancel the event - event.setCancelled(true); - event.setCancelReason("RATE LIMITED"); - - // Send an error response back to the client - Response response = exchange.response(); - if (!response.headersSent()) exchange.response().setCode(429); - response.print(Json.empty() - .set("status", "429") - .set("message", "You have been rate limited!")); - } - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/listeners/StartupListener.java b/src/main/java/de/craftsblock/cnet/modules/security/listeners/StartupListener.java deleted file mode 100644 index cee52c6..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/listeners/StartupListener.java +++ /dev/null @@ -1,32 +0,0 @@ -package de.craftsblock.cnet.modules.security.listeners; - -import de.craftsblock.cnet.modules.security.auth.token.TokenManager; -import de.craftsblock.cnet.modules.security.auth.token.driver.storage.FileTokenStorageDriver; -import de.craftsblock.craftscore.event.EventHandler; -import de.craftsblock.craftscore.event.ListenerAdapter; -import de.craftsblock.craftsnet.autoregister.meta.AutoRegister; -import de.craftsblock.craftsnet.events.addons.AllAddonsLoadedEvent; - -/** - * Initializes security related components after all addons have been loaded. - * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @since 1.0.0-SNAPSHOT - */ -@AutoRegister -public class StartupListener implements ListenerAdapter { - - /** - * Handles the {@link AllAddonsLoadedEvent} to initialize the token storage driver if none is set. - * - * @param event The {@link AllAddonsLoadedEvent} triggered when all addons have been loaded. - */ - @EventHandler - public void handleAllAddonLoaded(AllAddonsLoadedEvent event) { - if (TokenManager.getDriver() != null) return; - TokenManager.setDriver(new FileTokenStorageDriver()); - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/ratelimit/RateLimitAdapter.java b/src/main/java/de/craftsblock/cnet/modules/security/ratelimit/RateLimitAdapter.java deleted file mode 100644 index 968029c..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/ratelimit/RateLimitAdapter.java +++ /dev/null @@ -1,164 +0,0 @@ -package de.craftsblock.cnet.modules.security.ratelimit; - -import de.craftsblock.craftsnet.api.http.Exchange; -import de.craftsblock.craftsnet.api.http.Request; -import de.craftsblock.craftsnet.api.http.Response; -import de.craftsblock.craftsnet.api.session.Session; -import org.jetbrains.annotations.Nullable; - -/** - * The {@link RateLimitAdapter} is an abstract class that defines the structure for rate limiting logic. - * It enforces rate limiting policies for incoming {@link Request}s by mapping them to {@link RateLimitIndex} objects. - * The adapter also manages configuration settings like maximum request count, expiration times, and response headers. - *

- * Subclasses must implement the {@link #adapt(Request, Session)} method to define custom rate limiting behavior. - *

- * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.2 - * @see RateLimitIndex - * @see RateLimitInfo - * @see Request - * @since 1.0.0-SNAPSHOT - */ -public abstract class RateLimitAdapter { - - /** - * The maximum allowed expiration time in milliseconds (31 days). - */ - public static final long MAX_EXPIRE_MILLIS = (long) 31 * 24 * 60 * 60 * 1000; - - private final String id; - private final long max; - private final long expire; - private final boolean headers; - - /** - * Constructs a new {@link RateLimitAdapter} with the specified ID and maximum requests. - * The expiration time defaults to 60 seconds, and headers are included in the response. - * - * @param id The ID of the adapter (must contain only alphabetic characters). - * @param max The maximum number of requests allowed within the expiration period. - * @throws IllegalStateException If the ID is invalid. - * @see #RateLimitAdapter(String, long, long) - */ - public RateLimitAdapter(String id, long max) { - this(id, max, 1000 * 60); - } - - /** - * Constructs a new {@link RateLimitAdapter} with the specified ID, maximum requests, and expiration time. - * Headers are included in the response by default. - * - * @param id The ID of the adapter (must contain only alphabetic characters). - * @param max The maximum number of requests allowed within the expiration period. - * @param expire The expiration time in milliseconds. - * @throws IllegalStateException If the ID is invalid. - * @see #RateLimitAdapter(String, long, long, boolean) - */ - public RateLimitAdapter(String id, long max, long expire) { - this(id, max, expire, true); - } - - /** - * Constructs a new {@link RateLimitAdapter} with the specified parameters. - * - * @param id The ID of the adapter (must contain only alphabetic characters). - * @param max The maximum number of requests allowed within the expiration period. - * @param expire The expiration time in milliseconds (must be greater than 0 and less than or equal to {@link #MAX_EXPIRE_MILLIS}). - * @param headers Whether the rate limiting headers should be included in the response. - * @throws IllegalStateException If the ID is invalid. - * @throws AssertionError If the expiration time is not within the allowed range. - */ - public RateLimitAdapter(String id, long max, long expire, boolean headers) { - if (!id.matches("^[a-zA-Z]+$")) - throw new IllegalStateException("Rate limiting adapter IDs may only contain letters! (Invalid ID: '" + id + - "', set for: " + getClass().getName() + ")"); - - if (expire <= 0 || expire > MAX_EXPIRE_MILLIS) - throw new IllegalArgumentException("The expire time must be greater than 0 and less or equal than " + - MAX_EXPIRE_MILLIS + "! (Got: " + expire + ")"); - - this.id = id.toUpperCase(); - this.max = max; - this.expire = expire; - this.headers = headers; - } - - /** - * Maps a {@link Request} to a {@link RateLimitIndex}, defining how rate limits are applied. - * Subclasses must override this method to provide custom mapping logic. - * - * @param request The incoming HTTP request. - * @param session The session storage associated with the request. - * @return A {@link RateLimitIndex} representing the rate limit for the request, or {@code null} if no rate limit applies. - */ - public abstract @Nullable RateLimitIndex adapt(Request request, Session session); - - /** - * Creates a new {@link RateLimitInfo} instance for this adapter. - * - * @return A new {@link RateLimitInfo} instance. - */ - public RateLimitInfo createInfo() { - return RateLimitInfo.of(this); - } - - /** - * Appends rate limit information as HTTP headers to the response of the given {@link Exchange}. - *

This method adds the following headers to the response:

- *
    - *
  • X-RateLimit-Limit: Indicates the maximum number of requests allowed within the rate limit.
  • - *
  • X-RateLimit-Remaining: Indicates the remaining number of requests that can be made before the rate limit is exceeded.
  • - *
  • X-RateLimit-Reset: Indicates the time in milliseconds until the rate limit resets.
  • - *
- * - * @param exchange The {@link Exchange} representing the current HTTP request and response. - * @param info The {@link RateLimitInfo} containing the rate limit details for the current request. - */ - public void appendToResponse(final Exchange exchange, final RateLimitInfo info) { - final Response response = exchange.response(); - - response.addHeader("X-RateLimit-Limit", getId() + "=" + getMax()); - response.addHeader("X-RateLimit-Remaining", getId() + "=" + Math.max(0, getMax() - info.times().get())); - response.addHeader("X-RateLimit-Reset", getId() + "=" + Math.max(0, info.expiresAt().get() - System.currentTimeMillis())); - } - - /** - * Indicates whether rate limiting information should be included in the response headers. - * - * @return {@code true} if headers should be included, {@code false} otherwise. - */ - public boolean shouldBeInResponse() { - return headers; - } - - /** - * Gets the ID of this adapter. - * - * @return The ID of the adapter. - */ - public String getId() { - return id; - } - - /** - * Gets the maximum number of requests allowed within the expiration period. - * - * @return The maximum number of requests. - */ - public long getMax() { - return max; - } - - /** - * Gets the expiration time in milliseconds for this rate limit. - * - * @return The expiration time in milliseconds. - */ - public long getExpireInMilliseconds() { - return expire; - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/ratelimit/RateLimitIndex.java b/src/main/java/de/craftsblock/cnet/modules/security/ratelimit/RateLimitIndex.java deleted file mode 100644 index f7344b3..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/ratelimit/RateLimitIndex.java +++ /dev/null @@ -1,98 +0,0 @@ -package de.craftsblock.cnet.modules.security.ratelimit; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Objects; - -/** - * The {@link RateLimitIndex} class represents a unique index for rate limiting purposes. - * It wraps an arbitrary {@link RateLimitIndex#source} object, which serves as the identifier for a rate limit. - *

- * This class is implemented as a record for immutability and concise representation. - *

- * - * @param source The object representing the source of the rate limit. - * This could be an IP address, user ID, or any other identifier. - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @see Objects - * @since 1.0.0-SNAPSHOT - */ -public record RateLimitIndex(@Nullable RateLimitAdapter adapter, @NotNull Object source) { - - /** - * Compares this {@link RateLimitIndex} with another object for equality. - * Two {@link RateLimitIndex} instances are considered equal if their {@link #source} fields are equal. - * - * @param o The object to compare with this {@link RateLimitIndex}. - * @return {@code true} if the objects are equal, {@code false} otherwise. - */ - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - RateLimitIndex that = (RateLimitIndex) o; - - if (this.isGlobal()) { - if (!that.isGlobal() || !Objects.equals(this.adapter(), that.adapter())) - return false; - - } else if (this.adapter == null || !this.adapter.equals(that.adapter)) - return false; - - return Objects.equals(this.source(), that.source()); - } - - /** - * Computes the hash code for this {@link RateLimitIndex}. - * The hash code is derived from the {@link #source} object. - * - * @return The hash code of this {@link RateLimitIndex}. - */ - @Override - public int hashCode() { - return Objects.hashCode(source); - } - - /** - * Checks whether this {@link RateLimitIndex} should be treated globally or - * per {@link RateLimitAdapter}. - * - * @return {@code true} if this {@link RateLimitIndex} should be treated globally, - * {@code false} otherwise. - */ - public boolean isGlobal() { - return this.adapter() == null; - } - - /** - * Factory method to create a new global {@link RateLimitIndex} instance, - * with an instance of {@link Object} as source. - * - * @param source The source object to use as the identifier for the rate limit. - * @return A new {@link RateLimitIndex} instance wrapping the specified {@link RateLimitIndex#source}. - */ - public static RateLimitIndex of(@NotNull Object source) { - return new RateLimitIndex(null, source); - } - - /** - * Factory method to create a new {@link RateLimitIndex} instance, - * with an instance of {@link RateLimitAdapter} and an {@link Object} as source. - * - *

- * When the {@link RateLimitAdapter} is set to {@code null}, the index should be - * treated globally. - *

- * - * @param adapter The {@link RateLimitAdapter} that created the index. - * @param source The source object to use as the identifier for the rate limit. - * @return A new {@link RateLimitIndex} instance wrapping the specified {@link RateLimitIndex#source}. - */ - public static RateLimitIndex of(@Nullable RateLimitAdapter adapter, @NotNull Object source) { - return new RateLimitIndex(adapter, source); - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/ratelimit/RateLimitInfo.java b/src/main/java/de/craftsblock/cnet/modules/security/ratelimit/RateLimitInfo.java deleted file mode 100644 index e47d9e4..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/ratelimit/RateLimitInfo.java +++ /dev/null @@ -1,96 +0,0 @@ -package de.craftsblock.cnet.modules.security.ratelimit; - -import java.util.concurrent.atomic.AtomicLong; - -/** - * The {@link RateLimitInfo} class encapsulates information about rate limiting for a specific {@link RateLimitAdapter}. - * It tracks the number of accesses, the expiration time, and provides mechanisms to enforce rate limiting rules. - *

- * This record is immutable in structure, with thread-safe handling of internal state using {@link AtomicLong}. - *

- * - * @param adapter The {@link RateLimitAdapter} that defines the rate limiting configuration. - * @param times An {@link AtomicLong} tracking the number of accesses. - * @param expiresAt An {@link AtomicLong} representing the expiration timestamp in milliseconds. - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @see RateLimitAdapter - * @see AtomicLong - * @since 1.0.0-SNAPSHOT - */ -public record RateLimitInfo(RateLimitAdapter adapter, AtomicLong times, AtomicLong expiresAt) { - - /** - * Gets the expiration time as a new {@link AtomicLong}. - * This prevents external modification of the internal expiration timestamp. - * - * @return A new {@link AtomicLong} representing the expiration timestamp. - */ - public AtomicLong expiresAt() { - return new AtomicLong(expiresAt.get()); - } - - /** - * Attempts to access the resource controlled by this rate limit. - *

- * If the rate limit is expired, it resets the state. If the access count exceeds the maximum allowed, - * the method returns {@code true}, indicating the rate limit has been exceeded. Otherwise, it increments - * the access count and returns {@code false}. - *

- * - * @return {@code true} if the rate limit is exceeded, {@code false} otherwise. - */ - public boolean access() { - if (resetIfExpired()) return false; - if (times().get() >= adapter.getMax()) return true; - - times().incrementAndGet(); - return false; - } - - /** - * Checks if the rate limit has expired and resets it if necessary. - * - * @return {@code true} if the rate limit was expired and has been reset, {@code false} otherwise. - */ - public boolean resetIfExpired() { - if (isExpired()) { - reset(); - return true; - } - - return false; - } - - /** - * Resets the rate limit by setting the access count to zero and updating the expiration timestamp. - */ - public void reset() { - times().set(0); - expiresAt.set(System.currentTimeMillis() + adapter().getExpireInMilliseconds()); - } - - /** - * Checks whether the rate limit has expired based on the current system time. - * - * @return {@code true} if the rate limit has expired, {@code false} otherwise. - */ - public boolean isExpired() { - return expiresAt.get() <= System.currentTimeMillis(); - } - - /** - * Creates a new {@link RateLimitInfo} instance with the specified {@link RateLimitAdapter}. - * The access count and expiration timestamp are initialized and the state is reset. - * - * @param adapter The {@link RateLimitAdapter} to associate with this rate limit information. - * @return A new {@link RateLimitInfo} instance. - */ - public static RateLimitInfo of(RateLimitAdapter adapter) { - RateLimitInfo info = new RateLimitInfo(adapter, new AtomicLong(-1), new AtomicLong(-1)); - info.reset(); - return info; - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/ratelimit/RateLimitManager.java b/src/main/java/de/craftsblock/cnet/modules/security/ratelimit/RateLimitManager.java deleted file mode 100644 index 28f03ee..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/ratelimit/RateLimitManager.java +++ /dev/null @@ -1,121 +0,0 @@ -package de.craftsblock.cnet.modules.security.ratelimit; - -import de.craftsblock.cnet.modules.security.CNetSecurity; -import de.craftsblock.cnet.modules.security.events.ratelimit.RateLimitExceededEvent; -import de.craftsblock.cnet.modules.security.utils.Manager; -import de.craftsblock.craftsnet.api.http.Exchange; -import de.craftsblock.craftsnet.api.http.Request; -import de.craftsblock.craftsnet.api.session.Session; -import org.jetbrains.annotations.NotNull; - -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Stream; - -/** - * The {@link RateLimitManager} manages rate limiting adapters and their associated indices. - * It handles the registration of adapters, checks for rate limiting conditions, and removes expired rate limit entries. - *

- * This class is thread-safe, using {@link ConcurrentHashMap} to store adapters and indices. - *

- * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @see RateLimitAdapter - * @see RateLimitIndex - * @see RateLimitInfo - * @since 1.0.0-SNAPSHOT - */ -public class RateLimitManager implements Manager { - - private final ConcurrentHashMap adapters = new ConcurrentHashMap<>(); - private final ConcurrentHashMap indices = new ConcurrentHashMap<>(); - - /** - * Registers a {@link RateLimitAdapter} to this manager. - * - * @param adapter The {@link RateLimitAdapter} to register. - * @throws IllegalStateException If an adapter with the same ID is already registered. - */ - public void register(@NotNull RateLimitAdapter adapter) { - String id = adapter.getId(); - if (adapters.containsKey(id)) - throw new IllegalStateException("Tried to register rate limit adapter with id " + id + " for " + adapter.getClass().getName() + - ", but this id is already taken by " + adapters.get(id).getClass().getName() + "!"); - - this.adapters.put(id, adapter); - } - - /** - * Unregisters a {@link RateLimitAdapter} from this manager. - * - * @param adapter The {@link RateLimitAdapter} to unregister. - */ - public void unregister(@NotNull RateLimitAdapter adapter) { - this.adapters.remove(adapter.getId()); - } - - /** - * Checks whether a {@link RateLimitAdapter} is registered with this manager. - * - * @param adapter The {@link RateLimitAdapter} to check. - * @return {@code true} if the adapter is registered, {@code false} otherwise. - */ - public boolean isRegistered(@NotNull RateLimitAdapter adapter) { - return this.adapters.containsKey(adapter.getId()); - } - - /** - * Determines whether the given {@link Exchange} is rate limited by any registered adapter. - * If rate limited, the appropriate headers are added to the response. - * - * @param exchange The {@link Exchange} to check for rate limiting. - * @return {@code true} if the request is rate limited, {@code false} otherwise. - */ - public boolean isRateLimited(@NotNull Exchange exchange) { - if (this.adapters.isEmpty()) return false; - - final Request request = exchange.request(); - final Session session = exchange.session(); - - List exceeded = new ArrayList<>(); - for (RateLimitAdapter adapter : adapters.values()) { - RateLimitIndex index = adapter.adapt(request, session); - if (index == null) continue; - - RateLimitInfo info = indices.computeIfAbsent(index, r -> adapter.createInfo()); - if (info.access()) exceeded.add(adapter); - - if (adapter.shouldBeInResponse()) - adapter.appendToResponse(exchange, info); - } - - if (exceeded.isEmpty()) return false; - - try { - CNetSecurity.callEvent(new RateLimitExceededEvent(exchange, exceeded)); - } catch (InvocationTargetException | IllegalAccessException e) { - throw new RuntimeException(e); - } - - return true; - } - - /** - * Cleans up expired rate limit entries from the indices map. - * This method uses parallel streams if the number of entries exceeds 100 for better performance. - */ - public void tick() { - Stream> stream; - if (indices.size() >= 100) stream = indices.entrySet().parallelStream(); - else stream = indices.entrySet().stream(); - - stream.filter(entry -> entry.getValue().isExpired()) - .forEach(entry -> indices.remove(entry.getKey())); - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/ratelimit/builtin/IPRateLimitAdapter.java b/src/main/java/de/craftsblock/cnet/modules/security/ratelimit/builtin/IPRateLimitAdapter.java deleted file mode 100644 index c93051a..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/ratelimit/builtin/IPRateLimitAdapter.java +++ /dev/null @@ -1,72 +0,0 @@ -package de.craftsblock.cnet.modules.security.ratelimit.builtin; - -import de.craftsblock.cnet.modules.security.ratelimit.RateLimitAdapter; -import de.craftsblock.cnet.modules.security.ratelimit.RateLimitIndex; -import de.craftsblock.craftsnet.api.http.Request; -import de.craftsblock.craftsnet.api.session.Session; -import org.jetbrains.annotations.Nullable; - -/** - * The {@link IPRateLimitAdapter} is a builtin implementation of {@link RateLimitAdapter}. - * It enforces rate limiting based on the client's IP address. - *

- * Each unique IP address is tracked as a {@link RateLimitIndex}, and rate limits are applied individually. - *

- * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.1 - * @see RateLimitAdapter - * @see RateLimitIndex - * @since 1.0.0-SNAPSHOT - */ -public class IPRateLimitAdapter extends RateLimitAdapter { - - /** - * The id of the {@link IPRateLimitAdapter}. - */ - public static final String ID = "IP"; - - /** - * Constructs a new {@link IPRateLimitAdapter} with the default rate limit of one request per period. - * - * @param max The maximum number of requests allowed within the expiration period. - */ - public IPRateLimitAdapter(long max) { - super(ID, max); - } - - /** - * Constructs a new {@link IPRateLimitAdapter} with the default rate limit of one request per period. - * - * @param max The maximum number of requests allowed within the expiration period. - * @param expire The expiration time in milliseconds (must be greater than 0 and less than or equal to {@link #MAX_EXPIRE_MILLIS}). - */ - public IPRateLimitAdapter(long max, long expire) { - super(ID, max, expire); - } - - /** - * Constructs a new {@link IPRateLimitAdapter} with the default rate limit of one request per period. - * - * @param max The maximum number of requests allowed within the expiration period. - * @param expire The expiration time in milliseconds (must be greater than 0 and less than or equal to {@link #MAX_EXPIRE_MILLIS}). - * @param headers Whether the rate limiting headers should be included in the response. - */ - public IPRateLimitAdapter(long max, long expire, boolean headers) { - super(ID, max, expire, headers); - } - - /** - * Adapts the given {@link Request} into a {@link RateLimitIndex} based on the client's IP address. - * - * @param request The {@link Request} to adapt. - * @param session The {@link Session} associated with the request. - * @return A {@link RateLimitIndex} representing the client's IP address, or {@code null} if adaptation fails. - */ - @Override - public @Nullable RateLimitIndex adapt(Request request, Session session) { - return RateLimitIndex.of(this, request.getIp()); - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/ratelimit/builtin/TokenRateLimitAdapter.java b/src/main/java/de/craftsblock/cnet/modules/security/ratelimit/builtin/TokenRateLimitAdapter.java deleted file mode 100644 index 47e0380..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/ratelimit/builtin/TokenRateLimitAdapter.java +++ /dev/null @@ -1,78 +0,0 @@ -package de.craftsblock.cnet.modules.security.ratelimit.builtin; - -import de.craftsblock.cnet.modules.security.auth.token.Token; -import de.craftsblock.cnet.modules.security.ratelimit.RateLimitAdapter; -import de.craftsblock.cnet.modules.security.ratelimit.RateLimitIndex; -import de.craftsblock.craftsnet.api.http.Request; -import de.craftsblock.craftsnet.api.session.Session; -import org.jetbrains.annotations.Nullable; - -/** - * The {@link TokenRateLimitAdapter} is a builtin implementation of {@link RateLimitAdapter}. - * It enforces rate limiting based on the authentication token stored in the {@link Session}. - *

- * Each unique token is tracked as a {@link RateLimitIndex}, and rate limits are applied individually. - *

- * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.1 - * @see RateLimitAdapter - * @see RateLimitIndex - * @see Token - * @since 1.0.0-SNAPSHOT - */ -public class TokenRateLimitAdapter extends RateLimitAdapter { - - /** - * The id of the {@link TokenRateLimitAdapter}. - */ - public static final String ID = "TOKEN"; - - /** - * Constructs a new {@link TokenRateLimitAdapter} with the default rate limit of 60 requests per period. - * - * @param max The maximum number of requests allowed within the expiration period. - */ - public TokenRateLimitAdapter(long max) { - super(ID, max); - } - - /** - * Constructs a new {@link TokenRateLimitAdapter} with the default rate limit of 60 requests per period. - * - * @param max The maximum number of requests allowed within the expiration period. - * @param expire The expiration time in milliseconds (must be greater than 0 and less than or equal to {@link #MAX_EXPIRE_MILLIS}). - */ - public TokenRateLimitAdapter(long max, long expire) { - super(ID, max, expire); - } - - /** - * Constructs a new {@link TokenRateLimitAdapter} with the default rate limit of 60 requests per period. - * - * @param max The maximum number of requests allowed within the expiration period. - * @param expire The expiration time in milliseconds (must be greater than 0 and less than or equal to {@link #MAX_EXPIRE_MILLIS}). - * @param headers Whether the rate limiting headers should be included in the response. - */ - public TokenRateLimitAdapter(long max, long expire, boolean headers) { - super(ID, max, expire, headers); - } - - /** - * Adapts the given {@link Request} into a {@link RateLimitIndex} based on the authentication token stored in the {@link Session}. - *

- * If the session storage does not contain a valid authentication token, the method returns {@code null}. - *

- * - * @param request The {@link Request} to adapt. - * @param session The {@link Session} associated with the request, expected to contain the authentication token. - * @return A {@link RateLimitIndex} representing the token, or {@code null} if no token is found. - */ - @Override - public @Nullable RateLimitIndex adapt(Request request, Session session) { - if (!session.containsKey("auth.token")) return null; - return RateLimitIndex.of(this, session.getAsType("auth.token", Token.class)); - } - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/utils/Entity.java b/src/main/java/de/craftsblock/cnet/modules/security/utils/Entity.java deleted file mode 100644 index 1e64c55..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/utils/Entity.java +++ /dev/null @@ -1,26 +0,0 @@ -package de.craftsblock.cnet.modules.security.utils; - -import de.craftsblock.craftscore.json.Json; - -/** - * This interface defines the contract for any class that can be serialized - * into a {@link Json} object. It serves as a common type for entities - * that need to be converted to JSON format. - * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @since 1.0.0 - */ -public interface Entity { - - /** - * Serializes the current object into a {@link Json} representation. - * Implementing classes should define how their internal state is converted - * into a JSON format. - * - * @return a {@link Json} object representing the serialized state of the entity. - */ - Json serialize(); - -} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/utils/Manager.java b/src/main/java/de/craftsblock/cnet/modules/security/utils/Manager.java deleted file mode 100644 index b889c73..0000000 --- a/src/main/java/de/craftsblock/cnet/modules/security/utils/Manager.java +++ /dev/null @@ -1,17 +0,0 @@ -package de.craftsblock.cnet.modules.security.utils; - -/** - * The {@link Manager} interface serves as a marker for classes that manage specific functionalities - * or resources within the AccessController addon. This interface itself does not define any methods but - * represents the general contract for all managers in the system. - * - *

Classes implementing this interface may provide methods for adding, removing, or querying - * managed entities.

- * - * @author Philipp Maywald - * @author CraftsBlock - * @version 1.0.0 - * @since 1.0.0-SNAPSHOT - */ -public interface Manager { -} diff --git a/src/main/resources/addon.json b/src/main/resources/addon.json index de55a7b..2561064 100644 --- a/src/main/resources/addon.json +++ b/src/main/resources/addon.json @@ -1,6 +1,6 @@ { "name": "CNetSecurity", - "main": "de.craftsblock.cnet.modules.security.AddonEntrypoint", + "main": "de.craftsblock.cnet.modules.security.CraftsNetSecurity", "authors": [ "Philipp Maywald", "CraftsBlock" @@ -11,7 +11,6 @@ "https://repo.craftsblock.de/releases" ], "dependencies": [ - "de.craftsblock.craftscore:sql:3.8.7", - "org.springframework.security:spring-security-crypto:6.5.0" + "org.springframework.security:spring-security-crypto:7.0.2" ] } \ No newline at end of file From 738167b00760775d0017fa3ffb0bb227894b2833 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Sun, 18 Jan 2026 14:36:34 +0100 Subject: [PATCH 03/35] feat: Add getType to AuthResult --- .../de/craftsblock/cnet/modules/security/auth/AuthResult.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/AuthResult.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/AuthResult.java index d7cf5cf..8a3cdf1 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/auth/AuthResult.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/auth/AuthResult.java @@ -40,6 +40,10 @@ public String getReason() { return reason; } + public Type getType() { + return type; + } + public static AuthResult ok() { return new AuthResult(Type.OK); } From b386fb54df511b24c02e392688b7d9c5bc2a6afa Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Sun, 18 Jan 2026 14:37:06 +0100 Subject: [PATCH 04/35] feat: First preview of the new token --- .../cnet/modules/security/token/Token.java | 70 +++++++++++++++++++ .../security/token/TokenDataContainer.java | 68 ++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/Token.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/TokenDataContainer.java diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/Token.java b/src/main/java/de/craftsblock/cnet/modules/security/token/Token.java new file mode 100644 index 0000000..e2e706c --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/Token.java @@ -0,0 +1,70 @@ +package de.craftsblock.cnet.modules.security.token; + +import de.craftsblock.craftscore.json.Json; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnmodifiableView; +import org.springframework.security.crypto.bcrypt.BCrypt; + +import java.util.*; +import java.util.stream.IntStream; + +public record Token(long id, @NotNull String hash, @NotNull Collection scopes, @NotNull TokenDataContainer tokenDataContainer) { + + public Token(long id, @NotNull String hash, @NotNull Collection scopes, @NotNull TokenDataContainer tokenDataContainer) { + this.id = id; + this.hash = hash; + this.scopes = Collections.unmodifiableCollection(scopes); + this.tokenDataContainer = tokenDataContainer; + } + + @Override + public @NotNull @UnmodifiableView Collection scopes() { + return scopes; + } + + public boolean validate(@NotNull String secret) { + return BCrypt.checkpw(secret, hash); + } + + public Json toJson() { + Json json = Json.empty() + .set("id", this.id) + .set("hash", this.hash) + .set("scopes", this.scopes); + + Map serializedTokenDataContainer = this.tokenDataContainer.serializeToMap(); + serializedTokenDataContainer.forEach((key, data) -> json.set( + "token_data_container." + key, + IntStream.range(0, data.length) + .mapToObj(i -> data[i]) + .toList() + )); + + return json; + } + + public static Token fromJson(Json json) { + Json jsonTokenDataContainer = json.getJson("token_data_container", Json.empty()); + Map serializedTokenDataContainer = new HashMap<>(); + + jsonTokenDataContainer.keySet().forEach(key -> { + List dataList = (List) jsonTokenDataContainer.getByteList(key); + byte[] data = new byte[dataList.size()]; + + for (int i = 0; i < dataList.size(); i++) { + data[i] = dataList.get(i); + } + + serializedTokenDataContainer.put(key, data); + }); + + TokenDataContainer tokenDataContainer = new TokenDataContainer(serializedTokenDataContainer); + return new Token( + json.getLong("id"), + json.getString("hash"), + json.getStringList("scopes"), + tokenDataContainer + ); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/TokenDataContainer.java b/src/main/java/de/craftsblock/cnet/modules/security/token/TokenDataContainer.java new file mode 100644 index 0000000..a02faef --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/TokenDataContainer.java @@ -0,0 +1,68 @@ +package de.craftsblock.cnet.modules.security.token; + +import de.craftsblock.craftscore.buffer.BufferUtil; +import de.craftsblock.craftscore.buffer.ObjectSerializer; +import de.craftsblock.craftsnet.utils.reflection.TypeUtils; +import org.jetbrains.annotations.NotNull; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class TokenDataContainer extends ConcurrentHashMap { + + public TokenDataContainer() { + } + + public TokenDataContainer(byte[] data) { + BufferUtil buffer = BufferUtil.wrap(data); + + while (buffer.hasRemainingBytes()) { + String key = buffer.getUtf(); + byte[] value = buffer.getNBytes(buffer.getVarInt()); + this.put(key, ObjectSerializer.deserialize(value)); + } + } + + public TokenDataContainer(Map data) { + data.forEach((key, value) -> this.put(key, ObjectSerializer.deserialize(value))); + } + + public T getTyped(@NotNull String key, @NotNull Class type) { + return this.getOrDefaultTyped(key, type, null); + } + + @SuppressWarnings("unchecked") + public T getOrDefaultTyped(@NotNull String key, @NotNull Class type, T orElse) { + if (!containsKey(key)) return orElse; + return (T) get(key); + } + + public boolean isType(@NotNull String key, @NotNull Class type) { + if (!containsKey(key)) return false; + return TypeUtils.isAssignable(type, get(key).getClass()); + } + + public byte[] serializeToBytes() { + BufferUtil buffer = BufferUtil.allocate(0); + + this.forEach((key, value) -> { + byte[] serialized = ObjectSerializer.serialize(value); + + buffer.ensure(8 + key.getBytes(StandardCharsets.UTF_8).length + serialized.length) + .putUtf(key) + .putVarInt(serialized.length) + .with(raw -> raw.put(serialized)); + }); + + return buffer.trim().toByteArray(); + } + + public Map serializeToMap() { + Map serialized = new HashMap<>(); + this.forEach((key, value) -> serialized.put(key, ObjectSerializer.serialize(value))); + return serialized; + } + +} From a7ccb77bfedc7cbeae382ac567800915c90dd6c2 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Sun, 18 Jan 2026 14:37:32 +0100 Subject: [PATCH 05/35] feat: Add scope filters --- .../security/token/scope/RequireScope.java | 18 +++++ .../token/scope/ScopeRequirement.java | 70 +++++++++++++++++++ .../token/scope/ScopeResolveMiddleware.java | 60 ++++++++++++++++ .../security/token/scope/ScopeResult.java | 9 +++ 4 files changed, 157 insertions(+) create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/scope/RequireScope.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeRequirement.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResolveMiddleware.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResult.java diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/scope/RequireScope.java b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/RequireScope.java new file mode 100644 index 0000000..8eaba42 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/RequireScope.java @@ -0,0 +1,18 @@ +package de.craftsblock.cnet.modules.security.token.scope; + +import de.craftsblock.craftsnet.api.requirements.meta.RequirementMeta; +import de.craftsblock.craftsnet.api.requirements.meta.RequirementStore; +import de.craftsblock.craftsnet.api.requirements.meta.RequirementType; + +import java.lang.annotation.*; + +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@RequirementMeta(type = RequirementType.STORING) +public @interface RequireScope { + + @RequirementStore + String[] value(); + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeRequirement.java b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeRequirement.java new file mode 100644 index 0000000..ed6bc8a --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeRequirement.java @@ -0,0 +1,70 @@ +package de.craftsblock.cnet.modules.security.token.scope; + +import de.craftsblock.cnet.modules.security.token.Token; +import de.craftsblock.craftsnet.addon.meta.Startup; +import de.craftsblock.craftsnet.api.RouteRegistry; +import de.craftsblock.craftsnet.api.http.Request; +import de.craftsblock.craftsnet.api.requirements.web.WebRequirement; +import de.craftsblock.craftsnet.api.requirements.websocket.WebSocketRequirement; +import de.craftsblock.craftsnet.api.utils.Context; +import de.craftsblock.craftsnet.api.websocket.WebSocketClient; +import de.craftsblock.craftsnet.autoregister.meta.AutoRegister; +import org.jetbrains.annotations.ApiStatus; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.List; + +@ApiStatus.Internal +public sealed interface ScopeRequirement + permits ScopeRequirement.Http, ScopeRequirement.WebSocket { + + default boolean handleRequire(Context context, RouteRegistry.EndpointMapping mapping) { + if (!context.containsKey(Token.class)) { + return true; + } + + if (!mapping.isPresent(getAnnotation(), "value")) { + Token token = context.getTyped(Token.class); + List scopes = mapping.getRequirements(getAnnotation(), "value"); + context.put(new ScopeResult(scopes, token.scopes().containsAll(scopes))); + } else { + context.put(new ScopeResult(Collections.emptyList(), true)); + } + + return true; + } + + Class getAnnotation(); + + @ApiStatus.Internal + @AutoRegister(startup = Startup.LOAD) + final class Http extends WebRequirement implements ScopeRequirement { + + public Http() { + super(RequireScope.class); + } + + @Override + public boolean applies(Request request, RouteRegistry.EndpointMapping mapping) { + return handleRequire(request.getExchange().context(), mapping); + } + + } + + @ApiStatus.Internal + @AutoRegister(startup = Startup.LOAD) + final class WebSocket extends WebSocketRequirement implements ScopeRequirement { + + public WebSocket() { + super(RequireScope.class); + } + + @Override + public boolean applies(WebSocketClient webSocketClient, RouteRegistry.EndpointMapping mapping) { + return handleRequire(webSocketClient.getContext(), mapping); + } + + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResolveMiddleware.java b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResolveMiddleware.java new file mode 100644 index 0000000..b9fca6e --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResolveMiddleware.java @@ -0,0 +1,60 @@ +package de.craftsblock.cnet.modules.security.token.scope; + +import de.craftsblock.craftscore.event.EventHandler; +import de.craftsblock.craftscore.event.EventPriority; +import de.craftsblock.craftscore.event.ListenerAdapter; +import de.craftsblock.craftscore.json.Json; +import de.craftsblock.craftsnet.addon.meta.Startup; +import de.craftsblock.craftsnet.api.BaseExchange; +import de.craftsblock.craftsnet.api.http.Exchange; +import de.craftsblock.craftsnet.api.utils.Context; +import de.craftsblock.craftsnet.api.websocket.SocketExchange; +import de.craftsblock.craftsnet.autoregister.meta.AutoRegister; +import de.craftsblock.craftsnet.events.EventWithCancelReason; +import de.craftsblock.craftsnet.events.requests.routes.RouteRequestEvent; +import de.craftsblock.craftsnet.events.sockets.ClientConnectEvent; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.Consumer; + +@ApiStatus.Internal +@AutoRegister(startup = Startup.LOAD) +public class ScopeResolveMiddleware implements ListenerAdapter { + + private final Json MISSING_SCOPES_MESSAGE = Json.empty() + .set("success", false) + .set("error.code", 403) + .set("error.message", "Not allowed!"); + + private void handle(BaseExchange exchange, EventWithCancelReason event, T subject, Consumer onFailure) { + Context context = exchange.context(); + if (context == null || !context.containsKey(ScopeResult.class)) { + return; + } + + try { + final ScopeResult result = context.getTyped(ScopeResult.class); + if (!result.allScopesPresent()) { + event.setCancelled(true); + event.setCancelReason("AUTH FAILED"); + + onFailure.accept(subject); + } + } finally { + context.remove(ScopeResult.class); + } + } + + @EventHandler(priority = EventPriority.HIGH, ignoreWhenCancelled = true) + public void handleRequest(RouteRequestEvent event) { + final Exchange exchange = event.getExchange(); + handle(exchange, event, exchange.response(), response -> response.print(MISSING_SCOPES_MESSAGE)); + } + + @EventHandler(priority = EventPriority.HIGH, ignoreWhenCancelled = true) + public void handleWebSocketConnect(ClientConnectEvent event) { + final SocketExchange exchange = event.getExchange(); + handle(exchange, event, exchange.client(), client -> client.sendMessage(MISSING_SCOPES_MESSAGE)); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResult.java b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResult.java new file mode 100644 index 0000000..43f794b --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResult.java @@ -0,0 +1,9 @@ +package de.craftsblock.cnet.modules.security.token.scope; + +import org.jetbrains.annotations.ApiStatus; + +import java.util.List; + +@ApiStatus.Internal +record ScopeResult(List scopes, boolean allScopesPresent) { +} From f23b3794513392de8944d87ad1d2bd28220f5d93 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Fri, 23 Jan 2026 11:56:58 +0100 Subject: [PATCH 06/35] feat: Add a file token store driver --- .../modules/security/CraftsNetSecurity.java | 7 + .../token/driver/TokenStoreDriver.java | 26 +++ .../driver/file/FileTokenStoreDriver.java | 170 ++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java diff --git a/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java b/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java index 372a0b6..cdcae84 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java @@ -1,10 +1,13 @@ package de.craftsblock.cnet.modules.security; import de.craftsblock.cnet.modules.security.auth.AuthChain; +import de.craftsblock.cnet.modules.security.token.driver.TokenStoreDriver; +import de.craftsblock.cnet.modules.security.token.driver.file.FileTokenStoreDriver; import de.craftsblock.craftsnet.CraftsNet; import de.craftsblock.craftsnet.addon.Addon; import de.craftsblock.craftsnet.addon.meta.annotations.Meta; import de.craftsblock.craftsnet.builder.ActivateType; +import de.craftsblock.craftsnet.logging.Logger; import java.io.IOException; @@ -13,6 +16,8 @@ public class CraftsNetSecurity extends Addon { private AuthChain authChain; + private TokenStoreDriver tokenStoreDriver; + public static void main(String[] args) throws IOException { CraftsNet.create(CraftsNetSecurity.class) .withWebServer(ActivateType.ENABLED) @@ -25,6 +30,7 @@ public static void main(String[] args) throws IOException { @Override public void onLoad() { this.authChain = new AuthChain(); + this.tokenStoreDriver = new FileTokenStoreDriver(getDataPath().resolve("tokens.json")); } @Override @@ -35,6 +41,7 @@ public void onEnable() { @Override public void onDisable() { super.onDisable(); + this.tokenStoreDriver.close(); } public static AuthChain getAuthChain() { diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java new file mode 100644 index 0000000..30d7c4d --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java @@ -0,0 +1,26 @@ +package de.craftsblock.cnet.modules.security.token.driver; + +import de.craftsblock.cnet.modules.security.token.Token; + +import java.util.Collection; + +public interface TokenStoreDriver extends AutoCloseable { + + boolean exists(long id); + + Token load(long id); + + void save(Token token); + + default void delete(Token token) { + this.delete(token.id()); + } + + void delete(long id); + + Collection getAllTokenIds(); + + @Override + void close(); + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java new file mode 100644 index 0000000..e2338de --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java @@ -0,0 +1,170 @@ +package de.craftsblock.cnet.modules.security.token.driver.file; + +import de.craftsblock.cnet.modules.security.CraftsNetSecurity; +import de.craftsblock.cnet.modules.security.token.Token; +import de.craftsblock.cnet.modules.security.token.driver.TokenStoreDriver; +import de.craftsblock.craftscore.json.Json; +import de.craftsblock.craftscore.json.JsonParser; +import de.craftsblock.craftsnet.logging.Logger; +import de.craftsblock.craftsnet.utils.reflection.TypeUtils; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.*; +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +public class FileTokenStoreDriver implements TokenStoreDriver { + + public static final int WARN_AT_FILE_SIZE = 1024 * 1024 * 15; + + private final @NotNull Path tokensDirectory; + private final @NotNull Path tokensFile; + private final @NotNull AtomicReference tokens = new AtomicReference<>(); + + private final @NotNull Thread watchThread; + private final @NotNull WatchService watchService; + + private boolean closed = false; + + public FileTokenStoreDriver(@NotNull Path tokensFile) { + this.tokensFile = tokensFile; + this.tokensDirectory = tokensFile.toAbsolutePath().getParent(); + + try { + long size = Files.size(tokensFile); + if (size >= WARN_AT_FILE_SIZE) { + Logger logger = CraftsNetSecurity.getInstance().getLogger(); + logger.warning( + "The token store is larger than %s MB (%s MB), which may cause slowdowns!", + WARN_AT_FILE_SIZE / 1024 / 1024, size / 1024 / 1024 + ); + logger.warning("Please consider using a database."); + } + + this.reload(); + this.watchService = FileSystems.getDefault().newWatchService(); + this.tokensDirectory.register(this.watchService, StandardWatchEventKinds.ENTRY_MODIFY); + + this.watchThread = new Thread(() -> { + try { + WatchKey key; + while ((key = watchService.take()) != null) { + for (WatchEvent event : key.pollEvents()) { + if (!TypeUtils.isAssignable(Path.class, event.kind().type())) { + continue; + } + + Path path = (Path) event.context(); + Path realPath = tokensDirectory.resolve(path); + if (realPath.equals(tokensFile.toAbsolutePath())) { + this.reload(); + } + } + key.reset(); + } + } catch (InterruptedException ignored) { + } + }, "Token file watcher"); + this.watchThread.start(); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read file: " + e.getMessage(), e); + } + } + + private void reload() { + ensureOpen(); + synchronized (this.tokens) { + if (this.tokens.get() != null) { + CraftsNetSecurity.getInstance().getLogger().debug("Detected file system change, " + + "reloading token file."); + } + + this.tokens.set(JsonParser.parse(tokensFile)); + } + } + + @Override + public boolean exists(long id) { + ensureOpen(); + synchronized (this.tokens) { + return this.tokens.get().contains(String.valueOf(id)); + } + } + + @Override + public Token load(long id) { + ensureOpen(); + Json token; + + synchronized (this.tokens) { + token = this.tokens.get().getJson(String.valueOf(id)); + } + + if (token == null) { + throw new IllegalStateException("Token for id %s not found".formatted(id)); + } + + return Token.fromJson(token); + } + + @Override + public void save(@NotNull Token token) { + ensureOpen(); + Json json = token.toJson(); + + synchronized (this.tokens) { + this.tokens.get().set(String.valueOf(token.id()), json); + this.tokens.get().save(tokensFile); + } + } + + @Override + public void delete(long id) { + ensureOpen(); + synchronized (this.tokens) { + this.tokens.get().remove(String.valueOf(id)); + } + } + + @Override + public Collection getAllTokenIds() { + ensureOpen(); + Set stringIds; + + synchronized (this.tokens) { + stringIds = this.tokens.get().keySet(); + } + + return stringIds.stream() + .map(Long::parseLong) + .toList(); + } + + public void ensureOpen() { + if (closed) { + throw new IllegalStateException("No operations allowed after closure!"); + } + } + + @Override + public void close() { + try { + this.watchThread.interrupt(); + this.watchThread.join(); + } catch (InterruptedException ignored) { + } + + try { + this.watchService.close(); + } catch (IOException e) { + throw new UncheckedIOException("Failed to close: " + e.getMessage(), e); + } + + this.tokens.set(null); + this.closed = true; + } + +} From 72e59eaaabf75046e5552061798efd130fb5a880 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Fri, 23 Jan 2026 11:57:16 +0100 Subject: [PATCH 07/35] chore: Allow RequireScope on methods --- .../cnet/modules/security/token/scope/RequireScope.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/scope/RequireScope.java b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/RequireScope.java index 8eaba42..7074c19 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/scope/RequireScope.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/RequireScope.java @@ -7,8 +7,8 @@ import java.lang.annotation.*; @Documented -@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) @RequirementMeta(type = RequirementType.STORING) public @interface RequireScope { From f69ab5e35e91dabde5f02c1c850488ab6fa6bf43 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Fri, 23 Jan 2026 11:57:39 +0100 Subject: [PATCH 08/35] chore: Bump version of craftscore to 3.8.13-pre7 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index bf10f6f..51b86ac 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ dependencies { // CraftsBlock dependencies ---------------------------------------------------------------------------------------- // https://repo.craftsblock.de/#/releases/de/craftsblock/craftscore/bom - implementation platform("de.craftsblock.craftscore:bom:3.8.13-pre6") + implementation platform("de.craftsblock.craftscore:bom:3.8.13-pre7") // https://repo.craftsblock.de/#/releases/de/craftsblock/craftsnet implementation "de.craftsblock:craftsnet:3.7.0-pre7" From d4e6a6eab7828cedc0c02c3479f0baac9fb7efed Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Fri, 23 Jan 2026 11:58:21 +0100 Subject: [PATCH 09/35] docs: Bump minimum version of craftsnet to 3.7.0 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index feff86b..36b2533 100644 --- a/README.md +++ b/README.md @@ -10,5 +10,5 @@ | CraftsNet Version | Compatible | |-------------------|------------| -| >= 3.4.1-SNAPSHOT | ✅ | -| <= 3.4.0-SNAPSHOT | ❌ | +| >= 3.7.0 | ✅ | +| <= 3.7.0 | ❌ | From 935139365159d16333a4f418b881175d80c04f9c Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Fri, 23 Jan 2026 14:14:31 +0100 Subject: [PATCH 10/35] chore: Default to file token store driver --- .../modules/security/CraftsNetSecurity.java | 11 +++++--- .../listener/TokenPostSetupListener.java | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java diff --git a/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java b/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java index cdcae84..ab42256 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java @@ -2,12 +2,10 @@ import de.craftsblock.cnet.modules.security.auth.AuthChain; import de.craftsblock.cnet.modules.security.token.driver.TokenStoreDriver; -import de.craftsblock.cnet.modules.security.token.driver.file.FileTokenStoreDriver; import de.craftsblock.craftsnet.CraftsNet; import de.craftsblock.craftsnet.addon.Addon; import de.craftsblock.craftsnet.addon.meta.annotations.Meta; import de.craftsblock.craftsnet.builder.ActivateType; -import de.craftsblock.craftsnet.logging.Logger; import java.io.IOException; @@ -30,7 +28,6 @@ public static void main(String[] args) throws IOException { @Override public void onLoad() { this.authChain = new AuthChain(); - this.tokenStoreDriver = new FileTokenStoreDriver(getDataPath().resolve("tokens.json")); } @Override @@ -48,6 +45,14 @@ public static AuthChain getAuthChain() { return getInstance().authChain; } + public static void setTokenStoreDriver(TokenStoreDriver tokenStoreDriver) { + getInstance().tokenStoreDriver = tokenStoreDriver; + } + + public static TokenStoreDriver getTokenStoreDriver() { + return getInstance().tokenStoreDriver; + } + public static CraftsNetSecurity getInstance() { return getAddon(CraftsNetSecurity.class); } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java b/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java new file mode 100644 index 0000000..e798af1 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java @@ -0,0 +1,26 @@ +package de.craftsblock.cnet.modules.security.token.listener; + +import de.craftsblock.cnet.modules.security.CraftsNetSecurity; +import de.craftsblock.cnet.modules.security.token.driver.TokenStoreDriver; +import de.craftsblock.cnet.modules.security.token.driver.file.FileTokenStoreDriver; +import de.craftsblock.craftscore.event.EventHandler; +import de.craftsblock.craftscore.event.EventPriority; +import de.craftsblock.craftscore.event.ListenerAdapter; +import de.craftsblock.craftsnet.events.addons.AllAddonsLoadedEvent; + +import java.nio.file.Path; + +public class TokenPostSetupListener implements ListenerAdapter { + + @EventHandler(priority = EventPriority.HIGHEST) + public void handleAllAddonsLoaded(AllAddonsLoadedEvent event) { + TokenStoreDriver currentDriver = CraftsNetSecurity.getTokenStoreDriver(); + if (currentDriver != null) { + return; + } + + Path dataPath = CraftsNetSecurity.getInstance().getDataPath(); + CraftsNetSecurity.setTokenStoreDriver(new FileTokenStoreDriver(dataPath.resolve("tokens.json"))); + } + +} From 213115ae3ed501583b6f8dbc6078dc912237071a Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Fri, 23 Jan 2026 21:24:13 +0100 Subject: [PATCH 11/35] fix: Use static reference for getAuthChain --- .../cnet/modules/security/auth/listener/AuthListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/listener/AuthListener.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/listener/AuthListener.java index 3438ec5..0fc1555 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/auth/listener/AuthListener.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/auth/listener/AuthListener.java @@ -17,7 +17,7 @@ sealed interface AuthListener permits PreRequestListener, WebSocketConnectLis default void authenticate(BaseExchange exchange, CancellableEvent event, T subject, BiConsumer onFailure) { CraftsNetSecurity addon = this.addon(); - AuthResult result = addon.getAuthChain().authenticate(exchange); + AuthResult result = CraftsNetSecurity.getAuthChain().authenticate(exchange); if (!result.isFailure()) { addon.getListenerRegistry().call( From 87f030fc1ca999d0eab5bb0848b6d478b22fd417 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Fri, 23 Jan 2026 21:25:05 +0100 Subject: [PATCH 12/35] fix: Scopes are now applicable for websockets --- .../{ScopeResult.java => ScopeRequest.java} | 2 +- .../token/scope/ScopeRequirement.java | 18 +++---- .../token/scope/ScopeResolveMiddleware.java | 49 +++++++++++++------ 3 files changed, 40 insertions(+), 29 deletions(-) rename src/main/java/de/craftsblock/cnet/modules/security/token/scope/{ScopeResult.java => ScopeRequest.java} (68%) diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResult.java b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeRequest.java similarity index 68% rename from src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResult.java rename to src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeRequest.java index 43f794b..abbb2a1 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResult.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeRequest.java @@ -5,5 +5,5 @@ import java.util.List; @ApiStatus.Internal -record ScopeResult(List scopes, boolean allScopesPresent) { +record ScopeRequest(List scopes) { } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeRequirement.java b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeRequirement.java index ed6bc8a..36bf237 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeRequirement.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeRequirement.java @@ -1,6 +1,5 @@ package de.craftsblock.cnet.modules.security.token.scope; -import de.craftsblock.cnet.modules.security.token.Token; import de.craftsblock.craftsnet.addon.meta.Startup; import de.craftsblock.craftsnet.api.RouteRegistry; import de.craftsblock.craftsnet.api.http.Request; @@ -19,17 +18,12 @@ public sealed interface ScopeRequirement permits ScopeRequirement.Http, ScopeRequirement.WebSocket { - default boolean handleRequire(Context context, RouteRegistry.EndpointMapping mapping) { - if (!context.containsKey(Token.class)) { - return true; - } - - if (!mapping.isPresent(getAnnotation(), "value")) { - Token token = context.getTyped(Token.class); + default boolean injectRequest(Context context, RouteRegistry.EndpointMapping mapping) { + if (mapping.isPresent(getAnnotation(), "value")) { List scopes = mapping.getRequirements(getAnnotation(), "value"); - context.put(new ScopeResult(scopes, token.scopes().containsAll(scopes))); + context.put(new ScopeRequest(scopes)); } else { - context.put(new ScopeResult(Collections.emptyList(), true)); + context.put(new ScopeRequest(Collections.emptyList())); } return true; @@ -47,7 +41,7 @@ public Http() { @Override public boolean applies(Request request, RouteRegistry.EndpointMapping mapping) { - return handleRequire(request.getExchange().context(), mapping); + return injectRequest(request.getExchange().context(), mapping); } } @@ -62,7 +56,7 @@ public WebSocket() { @Override public boolean applies(WebSocketClient webSocketClient, RouteRegistry.EndpointMapping mapping) { - return handleRequire(webSocketClient.getContext(), mapping); + return injectRequest(webSocketClient.getContext(), mapping); } } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResolveMiddleware.java b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResolveMiddleware.java index b9fca6e..c03fdf1 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResolveMiddleware.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResolveMiddleware.java @@ -1,5 +1,7 @@ package de.craftsblock.cnet.modules.security.token.scope; +import de.craftsblock.cnet.modules.security.token.Token; +import de.craftsblock.craftscore.event.CancellableEvent; import de.craftsblock.craftscore.event.EventHandler; import de.craftsblock.craftscore.event.EventPriority; import de.craftsblock.craftscore.event.ListenerAdapter; @@ -8,11 +10,12 @@ import de.craftsblock.craftsnet.api.BaseExchange; import de.craftsblock.craftsnet.api.http.Exchange; import de.craftsblock.craftsnet.api.utils.Context; +import de.craftsblock.craftsnet.api.websocket.ClosureCode; import de.craftsblock.craftsnet.api.websocket.SocketExchange; import de.craftsblock.craftsnet.autoregister.meta.AutoRegister; import de.craftsblock.craftsnet.events.EventWithCancelReason; import de.craftsblock.craftsnet.events.requests.routes.RouteRequestEvent; -import de.craftsblock.craftsnet.events.sockets.ClientConnectEvent; +import de.craftsblock.craftsnet.events.sockets.message.IncomingSocketMessageEvent; import org.jetbrains.annotations.ApiStatus; import java.util.function.Consumer; @@ -26,35 +29,49 @@ public class ScopeResolveMiddleware implements ListenerAdapter { .set("error.code", 403) .set("error.message", "Not allowed!"); - private void handle(BaseExchange exchange, EventWithCancelReason event, T subject, Consumer onFailure) { + private void handle(BaseExchange exchange, CancellableEvent event, T subject, Consumer onFailure) { Context context = exchange.context(); - if (context == null || !context.containsKey(ScopeResult.class)) { + System.out.println(context); + if (context == null || !context.containsKey(ScopeRequest.class)) { return; } - try { - final ScopeResult result = context.getTyped(ScopeResult.class); - if (!result.allScopesPresent()) { - event.setCancelled(true); - event.setCancelReason("AUTH FAILED"); - - onFailure.accept(subject); + if (!context.containsKey(Token.class)) { + event.setCancelled(true); + if (event instanceof EventWithCancelReason withCancelReason) { + withCancelReason.setCancelReason("NO TOKEN"); } - } finally { - context.remove(ScopeResult.class); + + return; } + + final Token token = context.getTyped(Token.class); + final ScopeRequest result = context.getTyped(ScopeRequest.class); + if (token.scopes().containsAll(result.scopes())) { + return; + } + + event.setCancelled(true); + if (event instanceof EventWithCancelReason withCancelReason) { + withCancelReason.setCancelReason("AUTH FAILED"); + } + + onFailure.accept(subject); } - @EventHandler(priority = EventPriority.HIGH, ignoreWhenCancelled = true) + @EventHandler(priority = EventPriority.HIGHEST, ignoreWhenCancelled = true) public void handleRequest(RouteRequestEvent event) { final Exchange exchange = event.getExchange(); handle(exchange, event, exchange.response(), response -> response.print(MISSING_SCOPES_MESSAGE)); } - @EventHandler(priority = EventPriority.HIGH, ignoreWhenCancelled = true) - public void handleWebSocketConnect(ClientConnectEvent event) { + @EventHandler(priority = EventPriority.HIGHEST, ignoreWhenCancelled = true) + public void handleWebSocketMessage(IncomingSocketMessageEvent event) { final SocketExchange exchange = event.getExchange(); - handle(exchange, event, exchange.client(), client -> client.sendMessage(MISSING_SCOPES_MESSAGE)); + handle(exchange, event, exchange.client(), client -> { + client.sendMessage(MISSING_SCOPES_MESSAGE); + client.close(ClosureCode.NORMAL, "Not allowed!"); + }); } } From eefb9563f5ab49fb18ab742f4e8cb5acc6685572 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Fri, 23 Jan 2026 21:25:32 +0100 Subject: [PATCH 13/35] chore: Create token file if not exists --- .../security/token/driver/file/FileTokenStoreDriver.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java index e2338de..3124715 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java @@ -34,6 +34,14 @@ public FileTokenStoreDriver(@NotNull Path tokensFile) { this.tokensDirectory = tokensFile.toAbsolutePath().getParent(); try { + if (Files.notExists(tokensDirectory)) { + Files.createDirectories(tokensDirectory); + } + + if (Files.notExists(tokensFile)) { + Files.createFile(tokensFile); + } + long size = Files.size(tokensFile); if (size >= WARN_AT_FILE_SIZE) { Logger logger = CraftsNetSecurity.getInstance().getLogger(); From 0e481ef3f7458f4a2656c58dc17864aca30c3c1b Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Fri, 23 Jan 2026 21:26:03 +0100 Subject: [PATCH 14/35] chore: Use byte[] for token validation --- .../java/de/craftsblock/cnet/modules/security/token/Token.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/Token.java b/src/main/java/de/craftsblock/cnet/modules/security/token/Token.java index e2e706c..a21907f 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/Token.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/Token.java @@ -22,7 +22,7 @@ public Token(long id, @NotNull String hash, @NotNull Collection scopes, return scopes; } - public boolean validate(@NotNull String secret) { + public boolean validate(byte @NotNull [] secret) { return BCrypt.checkpw(secret, hash); } From b0fd2b062a83ad3184c42c9c03ff5501e8d9349a Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Fri, 23 Jan 2026 21:26:29 +0100 Subject: [PATCH 15/35] fix: Serialize token data as empty if empty --- .../de/craftsblock/cnet/modules/security/token/Token.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/Token.java b/src/main/java/de/craftsblock/cnet/modules/security/token/Token.java index a21907f..3250722 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/Token.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/Token.java @@ -1,5 +1,6 @@ package de.craftsblock.cnet.modules.security.token; +import com.google.gson.JsonObject; import de.craftsblock.craftscore.json.Json; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.UnmodifiableView; @@ -40,6 +41,10 @@ public Json toJson() { .toList() )); + if (!json.contains("token_data_container")) { + json.set("token_data_container", new JsonObject()); + } + return json; } From f8e2b99ac3bfbbace091b8a70961419b315e7227 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Fri, 23 Jan 2026 21:27:16 +0100 Subject: [PATCH 16/35] feat: Add first preview of websocket token auth --- .../modules/security/CraftsNetSecurity.java | 9 ++ .../listener/WebSocketConnectListener.java | 2 - .../modules/security/token/TokenManager.java | 75 +++++++++++++++ .../adapter/WebSocketTokenAuthAdapter.java | 96 +++++++++++++++++++ .../listener/TokenPostSetupListener.java | 2 + .../modules/security/token/util/NewToken.java | 17 ++++ .../security/token/util/TokenParts.java | 4 + .../security/token/util/TokenUtil.java | 74 ++++++++++++++ 8 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/util/NewToken.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/util/TokenParts.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/util/TokenUtil.java diff --git a/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java b/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java index ab42256..3632a89 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java @@ -1,6 +1,8 @@ package de.craftsblock.cnet.modules.security; import de.craftsblock.cnet.modules.security.auth.AuthChain; +import de.craftsblock.cnet.modules.security.token.TokenManager; +import de.craftsblock.cnet.modules.security.token.adapter.WebSocketTokenAuthAdapter; import de.craftsblock.cnet.modules.security.token.driver.TokenStoreDriver; import de.craftsblock.craftsnet.CraftsNet; import de.craftsblock.craftsnet.addon.Addon; @@ -14,6 +16,7 @@ public class CraftsNetSecurity extends Addon { private AuthChain authChain; + private TokenManager tokenManager; private TokenStoreDriver tokenStoreDriver; public static void main(String[] args) throws IOException { @@ -28,6 +31,8 @@ public static void main(String[] args) throws IOException { @Override public void onLoad() { this.authChain = new AuthChain(); + this.authChain.append(new WebSocketTokenAuthAdapter()); + this.tokenManager = new TokenManager(); } @Override @@ -45,6 +50,10 @@ public static AuthChain getAuthChain() { return getInstance().authChain; } + public static TokenManager getTokenManager() { + return getInstance().tokenManager; + } + public static void setTokenStoreDriver(TokenStoreDriver tokenStoreDriver) { getInstance().tokenStoreDriver = tokenStoreDriver; } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/auth/listener/WebSocketConnectListener.java b/src/main/java/de/craftsblock/cnet/modules/security/auth/listener/WebSocketConnectListener.java index dc1ea7a..c31e856 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/auth/listener/WebSocketConnectListener.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/auth/listener/WebSocketConnectListener.java @@ -1,14 +1,12 @@ package de.craftsblock.cnet.modules.security.auth.listener; import de.craftsblock.cnet.modules.security.CraftsNetSecurity; -import de.craftsblock.craftscore.buffer.BufferUtil; import de.craftsblock.craftscore.event.EventHandler; import de.craftsblock.craftscore.event.EventPriority; import de.craftsblock.craftscore.event.ListenerAdapter; import de.craftsblock.craftscore.json.Json; import de.craftsblock.craftsnet.CraftsNet; import de.craftsblock.craftsnet.addon.meta.Startup; -import de.craftsblock.craftsnet.api.websocket.ClosureCode; import de.craftsblock.craftsnet.api.websocket.SocketExchange; import de.craftsblock.craftsnet.api.websocket.WebSocketClient; import de.craftsblock.craftsnet.autoregister.meta.AutoRegister; diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java b/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java new file mode 100644 index 0000000..5711380 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java @@ -0,0 +1,75 @@ +package de.craftsblock.cnet.modules.security.token; + +import de.craftsblock.cnet.modules.security.CraftsNetSecurity; +import de.craftsblock.cnet.modules.security.token.util.NewToken; +import de.craftsblock.cnet.modules.security.token.util.TokenParts; +import de.craftsblock.cnet.modules.security.token.util.TokenUtil; +import de.craftsblock.craftscore.utils.id.Snowflake; +import de.craftsblock.craftsnet.utils.PassphraseUtils; +import org.springframework.security.crypto.bcrypt.BCrypt; + +import java.util.Collection; +import java.util.List; + +public class TokenManager { + + public void persist(Token token) { + CraftsNetSecurity.getTokenStoreDriver().save(token); + } + + public void delete(Token token) { + CraftsNetSecurity.getTokenStoreDriver().delete(token); + } + + public Token getToken(String token) { + TokenParts parts = TokenUtil.splitToTokenParts(token); + if (parts == null) { + return null; + } + + try { + if (!CraftsNetSecurity.getTokenStoreDriver().exists(parts.id())) { + return null; + } + + Token realToken = CraftsNetSecurity.getTokenStoreDriver().load(parts.id()); + if (realToken == null || !realToken.validate(parts.secret())) { + return null; + } + + return realToken; + } finally { + PassphraseUtils.erase(parts.secret()); + } + } + + public NewToken newPersistedToken(String... scopes) { + return this.newPersistedToken(List.of(scopes)); + } + + public NewToken newPersistedToken(Collection scopes) { + NewToken newToken = newToken(scopes); + persist(newToken.token()); + return newToken; + } + + public NewToken newToken(String... scopes) { + return this.newToken(List.of(scopes)); + } + + public NewToken newToken(Collection scopes) { + long id = Snowflake.generate(); + byte[] secret = TokenUtil.newSecureSecret(); + String secretHash = BCrypt.hashpw(secret, BCrypt.gensalt()); + + try { + return new NewToken( + new Token(id, secretHash, scopes, new TokenDataContainer()), + TokenUtil.mergeTokenParts(id, secret) + ); + } finally { + PassphraseUtils.erase(secret); + } + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java new file mode 100644 index 0000000..1db066a --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java @@ -0,0 +1,96 @@ +package de.craftsblock.cnet.modules.security.token.adapter; + +import com.google.gson.JsonSyntaxException; +import de.craftsblock.cnet.modules.security.CraftsNetSecurity; +import de.craftsblock.cnet.modules.security.auth.AuthResult; +import de.craftsblock.cnet.modules.security.auth.adapter.AuthAdapter; +import de.craftsblock.cnet.modules.security.token.Token; +import de.craftsblock.craftscore.event.EventHandler; +import de.craftsblock.craftscore.event.EventPriority; +import de.craftsblock.craftscore.event.ListenerAdapter; +import de.craftsblock.craftscore.json.Json; +import de.craftsblock.craftscore.json.JsonParser; +import de.craftsblock.craftsnet.api.utils.Context; +import de.craftsblock.craftsnet.api.websocket.ClosureCode; +import de.craftsblock.craftsnet.api.websocket.Opcode; +import de.craftsblock.craftsnet.api.websocket.SocketExchange; +import de.craftsblock.craftsnet.api.websocket.WebSocketClient; +import de.craftsblock.craftsnet.autoregister.meta.AutoRegister; +import de.craftsblock.craftsnet.events.sockets.message.IncomingSocketMessageEvent; +import de.craftsblock.craftsnet.events.sockets.message.OutgoingSocketMessageEvent; + +@AutoRegister +public class WebSocketTokenAuthAdapter implements ListenerAdapter, AuthAdapter.WebSocket { + + private static final String MESSAGE_LITERAL_WRONG_AUTH = "Not allowed!"; + private static final Json MESSAGE_WRONG_AUTH = Json.empty() + .set("success", false) + .set("error.code", 400) + .set("error.message", MESSAGE_LITERAL_WRONG_AUTH); + + @Override + public AuthResult authenticate(SocketExchange exchange) { + exchange.context().put(new RequireAuth()); + return AuthResult.skip(); + } + + @EventHandler(priority = EventPriority.HIGH, ignoreWhenCancelled = true) + public void handleIncomingMessage(IncomingSocketMessageEvent event) { + final SocketExchange exchange = event.getExchange(); + final Context context = exchange.context(); + if (!context.containsKey(RequireAuth.class)) { + return; + } + + final WebSocketClient client = event.getClient(); + event.setCancelled(true); + if (!event.getOpcode().equals(Opcode.TEXT)) { + failAuth(client, "NOT TEXT"); + return; + } + + try { + String message = event.getUtf8(); + Json json = JsonParser.parse(message); + if (!json.contains("token")) { + failAuth(client, "NO TOKEN"); + return; + } + + Token token = CraftsNetSecurity.getTokenManager().getToken(json.getString("token")); + if (token == null) { + failAuth(client, "WRONG TOKEN"); + return; + } + + context.put(token); + context.remove(RequireAuth.class); + } catch (JsonSyntaxException ignored) { + failAuth(client, "NOT A JSON"); + } + } + + private void failAuth(WebSocketClient client, String reason) { + client.sendMessage(MESSAGE_WRONG_AUTH); + CraftsNetSecurity.getInstance().getLogger().debug("%s failed to authenticate \u001b[38;5;9m[%s]", client.getIp(), reason); + client.close(ClosureCode.NORMAL, MESSAGE_LITERAL_WRONG_AUTH); + } + + @EventHandler(ignoreWhenCancelled = true) + public void handleOutgoingMessage(OutgoingSocketMessageEvent event) { + final SocketExchange exchange = event.getExchange(); + if (!exchange.context().containsKey(RequireAuth.class)) { + return; + } + + if (event.getOpcode().equals(Opcode.TEXT) && event.getUtf8().equals(MESSAGE_WRONG_AUTH.toString())) { + return; + } + + event.setCancelled(true); + } + + private static class RequireAuth { + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java b/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java index e798af1..d313f97 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java @@ -6,10 +6,12 @@ import de.craftsblock.craftscore.event.EventHandler; import de.craftsblock.craftscore.event.EventPriority; import de.craftsblock.craftscore.event.ListenerAdapter; +import de.craftsblock.craftsnet.autoregister.meta.AutoRegister; import de.craftsblock.craftsnet.events.addons.AllAddonsLoadedEvent; import java.nio.file.Path; +@AutoRegister public class TokenPostSetupListener implements ListenerAdapter { @EventHandler(priority = EventPriority.HIGHEST) diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/util/NewToken.java b/src/main/java/de/craftsblock/cnet/modules/security/token/util/NewToken.java new file mode 100644 index 0000000..3f5f4db --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/util/NewToken.java @@ -0,0 +1,17 @@ +package de.craftsblock.cnet.modules.security.token.util; + +import de.craftsblock.cnet.modules.security.token.Token; +import de.craftsblock.craftsnet.utils.PassphraseUtils; + +public record NewToken(Token token, byte[] plain) implements AutoCloseable { + + public String plainStringify() { + return PassphraseUtils.stringify(plain); + } + + @Override + public void close() { + PassphraseUtils.erase(plain); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/util/TokenParts.java b/src/main/java/de/craftsblock/cnet/modules/security/token/util/TokenParts.java new file mode 100644 index 0000000..0299224 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/util/TokenParts.java @@ -0,0 +1,4 @@ +package de.craftsblock.cnet.modules.security.token.util; + +public record TokenParts(String prefix, long id, byte[] secret) { +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/util/TokenUtil.java b/src/main/java/de/craftsblock/cnet/modules/security/token/util/TokenUtil.java new file mode 100644 index 0000000..67ac0cd --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/util/TokenUtil.java @@ -0,0 +1,74 @@ +package de.craftsblock.cnet.modules.security.token.util; + +import de.craftsblock.cnet.modules.security.token.Token; +import de.craftsblock.craftscore.buffer.BufferUtil; +import de.craftsblock.craftsnet.utils.PassphraseUtils; + +import java.nio.charset.StandardCharsets; +import java.util.regex.Pattern; + +public class TokenUtil { + + private static String TOKEN_PREFIX = "cnet_"; + private static final byte[] TOKEN_PART_SEPARATOR_BYTES = ".".getBytes(StandardCharsets.UTF_8); + + private TokenUtil() { + } + + public static byte[] newSecureSecret() { + return PassphraseUtils.generateSecure(45, 70, false); + } + + public static byte[] mergeTokenParts(long id, byte[] secret) { + byte[] tokenPrefixBytes = TOKEN_PREFIX.getBytes(StandardCharsets.UTF_8); + byte[] idBytes = Long.toHexString(id).getBytes(StandardCharsets.UTF_8); + + BufferUtil buffer = BufferUtil.allocate(tokenPrefixBytes.length + idBytes.length + + TOKEN_PART_SEPARATOR_BYTES.length + secret.length); + try { + buffer.with(raw -> { + raw.put(tokenPrefixBytes); + raw.put(idBytes); + raw.put(TOKEN_PART_SEPARATOR_BYTES); + raw.put(secret); + }); + + return buffer.toByteArray(); + } finally { + PassphraseUtils.erase(tokenPrefixBytes); + PassphraseUtils.erase(idBytes); + } + } + + public static TokenParts splitToTokenParts(String token) { + if (!token.startsWith(TOKEN_PREFIX)) { + return null; + } + + String[] parts = token.replaceFirst("^" + Pattern.quote(TOKEN_PREFIX), "") + .split("\\.", 2); + if (parts.length != 2) { + return null; + } + + try { + String id = parts[0]; + long idLong = Long.parseLong(id, 16); + return new TokenParts(TOKEN_PREFIX.replace("_", ""), idLong, parts[1].getBytes()); + } catch (NumberFormatException ignored) { + return null; + } + } + + public static void setTokenPrefix(String tokenPrefix) { + TOKEN_PREFIX = tokenPrefix.replaceAll("_+", "_").trim(); + + if (TOKEN_PREFIX.endsWith("_")) return; + TOKEN_PREFIX += "_"; + } + + public static String getTokenPrefix() { + return TOKEN_PREFIX; + } + +} From 737b9b86f204430737949c6e95288e86474da979 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Fri, 23 Jan 2026 21:28:29 +0100 Subject: [PATCH 17/35] chore: Bump version of craftscore to 3.8.13-pre8 --- build.gradle | 2 +- .../de/craftsblock/cnet/modules/security/CraftsNetSecurity.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 51b86ac..787a7de 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ dependencies { // CraftsBlock dependencies ---------------------------------------------------------------------------------------- // https://repo.craftsblock.de/#/releases/de/craftsblock/craftscore/bom - implementation platform("de.craftsblock.craftscore:bom:3.8.13-pre7") + implementation platform("de.craftsblock.craftscore:bom:3.8.13-pre8") // https://repo.craftsblock.de/#/releases/de/craftsblock/craftsnet implementation "de.craftsblock:craftsnet:3.7.0-pre7" diff --git a/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java b/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java index 3632a89..bc669b8 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java @@ -2,6 +2,7 @@ import de.craftsblock.cnet.modules.security.auth.AuthChain; import de.craftsblock.cnet.modules.security.token.TokenManager; +import de.craftsblock.cnet.modules.security.token.adapter.HttpTokenAuthAdapter; import de.craftsblock.cnet.modules.security.token.adapter.WebSocketTokenAuthAdapter; import de.craftsblock.cnet.modules.security.token.driver.TokenStoreDriver; import de.craftsblock.craftsnet.CraftsNet; @@ -31,6 +32,7 @@ public static void main(String[] args) throws IOException { @Override public void onLoad() { this.authChain = new AuthChain(); + this.authChain.append(new HttpTokenAuthAdapter()); this.authChain.append(new WebSocketTokenAuthAdapter()); this.tokenManager = new TokenManager(); } From 796feb50edffef04497e825217ae196fb75e6941 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Fri, 23 Jan 2026 21:29:17 +0100 Subject: [PATCH 18/35] chore: Bump version of craftsnet to 3.7.0-pre8 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 787a7de..9d7407a 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ dependencies { implementation platform("de.craftsblock.craftscore:bom:3.8.13-pre8") // https://repo.craftsblock.de/#/releases/de/craftsblock/craftsnet - implementation "de.craftsblock:craftsnet:3.7.0-pre7" + implementation "de.craftsblock:craftsnet:3.7.0-pre8" // Third party dependencies ---------------------------------------------------------------------------------------- From ef5b069af14ab9e6108ffba9ea078da08ebe32e7 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Sat, 24 Jan 2026 13:47:20 +0100 Subject: [PATCH 19/35] feat: Add first preview of http token auth --- .../modules/security/CraftsNetSecurity.java | 2 +- .../token/adapter/HttpTokenAuthAdapter.java | 90 +++++++++++++++++++ .../token/adapter/HttpTokenAuthType.java | 28 ++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthAdapter.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthType.java diff --git a/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java b/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java index bc669b8..d5325ac 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java @@ -32,7 +32,7 @@ public static void main(String[] args) throws IOException { @Override public void onLoad() { this.authChain = new AuthChain(); - this.authChain.append(new HttpTokenAuthAdapter()); + this.authChain.append(new HttpTokenAuthAdapter(null)); this.authChain.append(new WebSocketTokenAuthAdapter()); this.tokenManager = new TokenManager(); } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthAdapter.java b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthAdapter.java new file mode 100644 index 0000000..aa02c4a --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthAdapter.java @@ -0,0 +1,90 @@ +package de.craftsblock.cnet.modules.security.token.adapter; + +import de.craftsblock.cnet.modules.security.CraftsNetSecurity; +import de.craftsblock.cnet.modules.security.auth.AuthResult; +import de.craftsblock.cnet.modules.security.auth.adapter.AuthAdapter; +import de.craftsblock.cnet.modules.security.token.Token; +import de.craftsblock.craftsnet.api.http.Exchange; +import de.craftsblock.craftsnet.api.http.Request; +import de.craftsblock.craftsnet.api.session.Session; + +import java.util.EnumMap; +import java.util.concurrent.atomic.AtomicReference; + +public class HttpTokenAuthAdapter implements AuthAdapter.Http { + + /** + * The expected authorization type for bearer tokens. + */ + public static final String HEADER_AUTH_TYPE = "bearer"; + + private final EnumMap authTypes; + + public HttpTokenAuthAdapter(EnumMap authTypes) { + this.authTypes = authTypes; + } + + @Override + public AuthResult authenticate(Exchange exchange) { + if (authTypes == null || authTypes.isEmpty()) { + return AuthResult.skip(); + } + + AtomicReference authResultReference = new AtomicReference<>(); + authTypes.forEach((authType, location) -> { + AuthResult previous = authResultReference.get(); + if (previous != null && !previous.isSkip()) { + return; + } + + AuthResult result = authenticate(exchange, authType, location); + authResultReference.set(result); + }); + + AuthResult result = authResultReference.get(); + if (result == null || !result.isOk()) { + return result != null && result.isFailure() ? result : AuthResult.failure("Not allowed!"); + } + + return AuthResult.ok(); + } + + public AuthResult authenticate(Exchange exchange, HttpTokenAuthType authType, String location) { + final Request request = exchange.request(); + final Session session = exchange.session(); + + String plainToken = switch (authType) { + case HEADER -> { + String auth_header = request.getHeader(location); + if (auth_header == null) { + yield null; + } + + String[] header = auth_header.split(" ", 2); + if (header.length != 2 || !HEADER_AUTH_TYPE.equalsIgnoreCase(header[0])) { + yield HEADER_AUTH_TYPE; + } + + yield header[1]; + } + case COOKIE -> { + var cookies = request.getCookies(); + yield cookies.containsKey(location) ? cookies.get(location).getValue() : null; + } + case SESSION -> session.getTyped(location, String.class); + }; + + if (plainToken == null || plainToken.isBlank()) { + return AuthResult.skip(); + } + + Token token = CraftsNetSecurity.getTokenManager().getToken(plainToken); + if (token == null) { + return AuthResult.failure("Not allowed! 2"); + } + + exchange.context().put(token); + return AuthResult.ok(); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthType.java b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthType.java new file mode 100644 index 0000000..9c7e7ae --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthType.java @@ -0,0 +1,28 @@ +package de.craftsblock.cnet.modules.security.token.adapter; + +public enum HttpTokenAuthType { + + HEADER("Authorization"), + COOKIE(), + SESSION(), + ; + + private final String defaultLocation; + + HttpTokenAuthType() { + this(null); + } + + HttpTokenAuthType(String defaultLocation) { + this.defaultLocation = defaultLocation; + } + + public String getDefaultLocation() { + return defaultLocation; + } + + public boolean hasDefaultLocation() { + return defaultLocation != null; + } + +} From 8fb96c346508e93f442d2b82f5ffad2d35255b32 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:14:07 +0100 Subject: [PATCH 20/35] fix: Fail and warn if not http token auth is set --- .../modules/security/token/adapter/HttpTokenAuthAdapter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthAdapter.java b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthAdapter.java index aa02c4a..9899fe2 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthAdapter.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthAdapter.java @@ -27,7 +27,8 @@ public HttpTokenAuthAdapter(EnumMap authTypes) { @Override public AuthResult authenticate(Exchange exchange) { if (authTypes == null || authTypes.isEmpty()) { - return AuthResult.skip(); + CraftsNetSecurity.getInstance().getLogger().warning("No http token auth type is set up!"); + return AuthResult.failure("Not allowed!"); } AtomicReference authResultReference = new AtomicReference<>(); From 0b8c11f3ea4c2ecdf2ba5e79397e180cf69d4ac8 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:14:26 +0100 Subject: [PATCH 21/35] fix: Better console output on scope mismatch --- .../token/scope/ScopeResolveMiddleware.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResolveMiddleware.java b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResolveMiddleware.java index c03fdf1..33546b0 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResolveMiddleware.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResolveMiddleware.java @@ -26,12 +26,11 @@ public class ScopeResolveMiddleware implements ListenerAdapter { private final Json MISSING_SCOPES_MESSAGE = Json.empty() .set("success", false) - .set("error.code", 403) + .set("error.code", 400) .set("error.message", "Not allowed!"); private void handle(BaseExchange exchange, CancellableEvent event, T subject, Consumer onFailure) { Context context = exchange.context(); - System.out.println(context); if (context == null || !context.containsKey(ScopeRequest.class)) { return; } @@ -53,7 +52,7 @@ private void handle(BaseExchange exchange, CancellableEvent event, T subject event.setCancelled(true); if (event instanceof EventWithCancelReason withCancelReason) { - withCancelReason.setCancelReason("AUTH FAILED"); + withCancelReason.setCancelReason("SCOPE MISMATCH"); } onFailure.accept(subject); @@ -62,7 +61,15 @@ private void handle(BaseExchange exchange, CancellableEvent event, T subject @EventHandler(priority = EventPriority.HIGHEST, ignoreWhenCancelled = true) public void handleRequest(RouteRequestEvent event) { final Exchange exchange = event.getExchange(); - handle(exchange, event, exchange.response(), response -> response.print(MISSING_SCOPES_MESSAGE)); + handle(exchange, event, exchange.response(), response -> { + if (!response.headersSent()) { + response.setCode(400); + } + + if (!response.sendingFile()) { + response.print(MISSING_SCOPES_MESSAGE); + } + }); } @EventHandler(priority = EventPriority.HIGHEST, ignoreWhenCancelled = true) From cb70f2adf0bbfbc928714eb3c9cf0184e9a78aba Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:44:56 +0100 Subject: [PATCH 22/35] feat: Add token events --- .../modules/security/token/TokenManager.java | 5 ++++- .../token/adapter/HttpTokenAuthAdapter.java | 2 ++ .../adapter/WebSocketTokenAuthAdapter.java | 2 ++ .../token/driver/TokenStoreDriver.java | 15 +++++++++++---- .../driver/file/FileTokenStoreDriver.java | 5 +++-- .../token/event/TokenCreateEvent.java | 11 +++++++++++ .../token/event/TokenDeleteEvent.java | 11 +++++++++++ .../security/token/event/TokenEvent.java | 19 +++++++++++++++++++ .../token/event/TokenPersistEvent.java | 11 +++++++++++ .../security/token/event/TokenUsedEvent.java | 11 +++++++++++ 10 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenCreateEvent.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenDeleteEvent.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenEvent.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenPersistEvent.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenUsedEvent.java diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java b/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java index 5711380..809daac 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java @@ -1,6 +1,7 @@ package de.craftsblock.cnet.modules.security.token; import de.craftsblock.cnet.modules.security.CraftsNetSecurity; +import de.craftsblock.cnet.modules.security.token.event.TokenCreateEvent; import de.craftsblock.cnet.modules.security.token.util.NewToken; import de.craftsblock.cnet.modules.security.token.util.TokenParts; import de.craftsblock.cnet.modules.security.token.util.TokenUtil; @@ -63,8 +64,10 @@ public NewToken newToken(Collection scopes) { String secretHash = BCrypt.hashpw(secret, BCrypt.gensalt()); try { + Token token = new Token(id, secretHash, scopes, new TokenDataContainer()); + CraftsNetSecurity.getInstance().getListenerRegistry().call(new TokenCreateEvent(token)); return new NewToken( - new Token(id, secretHash, scopes, new TokenDataContainer()), + token, TokenUtil.mergeTokenParts(id, secret) ); } finally { diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthAdapter.java b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthAdapter.java index 9899fe2..0d3414c 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthAdapter.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthAdapter.java @@ -4,6 +4,7 @@ import de.craftsblock.cnet.modules.security.auth.AuthResult; import de.craftsblock.cnet.modules.security.auth.adapter.AuthAdapter; import de.craftsblock.cnet.modules.security.token.Token; +import de.craftsblock.cnet.modules.security.token.event.TokenUsedEvent; import de.craftsblock.craftsnet.api.http.Exchange; import de.craftsblock.craftsnet.api.http.Request; import de.craftsblock.craftsnet.api.session.Session; @@ -84,6 +85,7 @@ public AuthResult authenticate(Exchange exchange, HttpTokenAuthType authType, St return AuthResult.failure("Not allowed! 2"); } + CraftsNetSecurity.getInstance().getListenerRegistry().call(new TokenUsedEvent(token)); exchange.context().put(token); return AuthResult.ok(); } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java index 1db066a..41d7d23 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java @@ -5,6 +5,7 @@ import de.craftsblock.cnet.modules.security.auth.AuthResult; import de.craftsblock.cnet.modules.security.auth.adapter.AuthAdapter; import de.craftsblock.cnet.modules.security.token.Token; +import de.craftsblock.cnet.modules.security.token.event.TokenUsedEvent; import de.craftsblock.craftscore.event.EventHandler; import de.craftsblock.craftscore.event.EventPriority; import de.craftsblock.craftscore.event.ListenerAdapter; @@ -63,6 +64,7 @@ public void handleIncomingMessage(IncomingSocketMessageEvent event) { return; } + CraftsNetSecurity.getInstance().getListenerRegistry().call(new TokenUsedEvent(token)); context.put(token); context.remove(RequireAuth.class); } catch (JsonSyntaxException ignored) { diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java index 30d7c4d..1e9e349 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java @@ -1,6 +1,9 @@ package de.craftsblock.cnet.modules.security.token.driver; +import de.craftsblock.cnet.modules.security.CraftsNetSecurity; import de.craftsblock.cnet.modules.security.token.Token; +import de.craftsblock.cnet.modules.security.token.event.TokenDeleteEvent; +import de.craftsblock.cnet.modules.security.token.event.TokenPersistEvent; import java.util.Collection; @@ -10,13 +13,17 @@ public interface TokenStoreDriver extends AutoCloseable { Token load(long id); - void save(Token token); + default void save(Token token) { + CraftsNetSecurity.getInstance().getListenerRegistry().call(new TokenPersistEvent(token)); + } - default void delete(Token token) { - this.delete(token.id()); + default void delete(long id) { + this.delete(load(id)); } - void delete(long id); + default void delete(Token token) { + CraftsNetSecurity.getInstance().getListenerRegistry().call(new TokenDeleteEvent(token)); + } Collection getAllTokenIds(); diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java index 3124715..fd112d4 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java @@ -130,10 +130,10 @@ public void save(@NotNull Token token) { } @Override - public void delete(long id) { + public void delete(Token token) { ensureOpen(); synchronized (this.tokens) { - this.tokens.get().remove(String.valueOf(id)); + this.tokens.get().remove(String.valueOf(token.id())); } } @@ -159,6 +159,7 @@ public void ensureOpen() { @Override public void close() { + ensureOpen(); try { this.watchThread.interrupt(); this.watchThread.join(); diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenCreateEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenCreateEvent.java new file mode 100644 index 0000000..17991f5 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenCreateEvent.java @@ -0,0 +1,11 @@ +package de.craftsblock.cnet.modules.security.token.event; + +import de.craftsblock.cnet.modules.security.token.Token; + +public final class TokenCreateEvent extends TokenEvent { + + public TokenCreateEvent(Token token) { + super(token); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenDeleteEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenDeleteEvent.java new file mode 100644 index 0000000..5125b75 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenDeleteEvent.java @@ -0,0 +1,11 @@ +package de.craftsblock.cnet.modules.security.token.event; + +import de.craftsblock.cnet.modules.security.token.Token; + +public final class TokenDeleteEvent extends TokenEvent { + + public TokenDeleteEvent(Token token) { + super(token); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenEvent.java new file mode 100644 index 0000000..ee28fed --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenEvent.java @@ -0,0 +1,19 @@ +package de.craftsblock.cnet.modules.security.token.event; + +import de.craftsblock.cnet.modules.security.token.Token; +import de.craftsblock.craftscore.event.Event; + +public abstract sealed class TokenEvent extends Event + permits TokenCreateEvent, TokenDeleteEvent, TokenPersistEvent, TokenUsedEvent { + + private final Token token; + + public TokenEvent(Token token) { + this.token = token; + } + + public Token getToken() { + return token; + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenPersistEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenPersistEvent.java new file mode 100644 index 0000000..4ccf915 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenPersistEvent.java @@ -0,0 +1,11 @@ +package de.craftsblock.cnet.modules.security.token.event; + +import de.craftsblock.cnet.modules.security.token.Token; + +public final class TokenPersistEvent extends TokenEvent { + + public TokenPersistEvent(Token token) { + super(token); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenUsedEvent.java b/src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenUsedEvent.java new file mode 100644 index 0000000..02606de --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/event/TokenUsedEvent.java @@ -0,0 +1,11 @@ +package de.craftsblock.cnet.modules.security.token.event; + +import de.craftsblock.cnet.modules.security.token.Token; + +public final class TokenUsedEvent extends TokenEvent { + + public TokenUsedEvent(Token token) { + super(token); + } + +} From 2c88a8821063406736c4bd15c20c7d3d330f78dd Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:59:02 +0100 Subject: [PATCH 23/35] chore: Rename getToken to getValidatedToken --- .../cnet/modules/security/token/TokenManager.java | 6 +++++- .../security/token/adapter/HttpTokenAuthAdapter.java | 2 +- .../security/token/adapter/WebSocketTokenAuthAdapter.java | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java b/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java index 809daac..8e80fc1 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java @@ -22,7 +22,7 @@ public void delete(Token token) { CraftsNetSecurity.getTokenStoreDriver().delete(token); } - public Token getToken(String token) { + public Token getValidatedToken(String token) { TokenParts parts = TokenUtil.splitToTokenParts(token); if (parts == null) { return null; @@ -75,4 +75,8 @@ public NewToken newToken(Collection scopes) { } } + public void clearCache() { + this.tokenCache.clear(); + } + } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthAdapter.java b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthAdapter.java index 0d3414c..0f038be 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthAdapter.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/HttpTokenAuthAdapter.java @@ -80,7 +80,7 @@ public AuthResult authenticate(Exchange exchange, HttpTokenAuthType authType, St return AuthResult.skip(); } - Token token = CraftsNetSecurity.getTokenManager().getToken(plainToken); + Token token = CraftsNetSecurity.getTokenManager().getValidatedToken(plainToken); if (token == null) { return AuthResult.failure("Not allowed! 2"); } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java index 41d7d23..f28cd04 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java @@ -58,7 +58,7 @@ public void handleIncomingMessage(IncomingSocketMessageEvent event) { return; } - Token token = CraftsNetSecurity.getTokenManager().getToken(json.getString("token")); + Token token = CraftsNetSecurity.getTokenManager().getValidatedToken(json.getString("token")); if (token == null) { failAuth(client, "WRONG TOKEN"); return; From 254414100da9f34a291c6a9baa3e69fc8241250d Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:00:09 +0100 Subject: [PATCH 24/35] feat: Add token caching --- .../modules/security/token/TokenManager.java | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java b/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java index 8e80fc1..7f2dbef 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java @@ -5,6 +5,7 @@ import de.craftsblock.cnet.modules.security.token.util.NewToken; import de.craftsblock.cnet.modules.security.token.util.TokenParts; import de.craftsblock.cnet.modules.security.token.util.TokenUtil; +import de.craftsblock.craftscore.cache.Cache; import de.craftsblock.craftscore.utils.id.Snowflake; import de.craftsblock.craftsnet.utils.PassphraseUtils; import org.springframework.security.crypto.bcrypt.BCrypt; @@ -14,6 +15,8 @@ public class TokenManager { + private final Cache tokenCache = new Cache<>(25); + public void persist(Token token) { CraftsNetSecurity.getTokenStoreDriver().save(token); } @@ -22,6 +25,20 @@ public void delete(Token token) { CraftsNetSecurity.getTokenStoreDriver().delete(token); } + public Token getToken(long id) { + if (tokenCache.containsKey(id)) { + return tokenCache.get(id); + } + + if (!CraftsNetSecurity.getTokenStoreDriver().exists(id)) { + return null; + } + + Token token = CraftsNetSecurity.getTokenStoreDriver().load(id); + tokenCache.put(token.id(), token); + return token; + } + public Token getValidatedToken(String token) { TokenParts parts = TokenUtil.splitToTokenParts(token); if (parts == null) { @@ -29,11 +46,7 @@ public Token getValidatedToken(String token) { } try { - if (!CraftsNetSecurity.getTokenStoreDriver().exists(parts.id())) { - return null; - } - - Token realToken = CraftsNetSecurity.getTokenStoreDriver().load(parts.id()); + Token realToken = getToken(parts.id()); if (realToken == null || !realToken.validate(parts.secret())) { return null; } From d15aa97ce7d7433077d7d3fda0740f0ed2d52104 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Tue, 27 Jan 2026 18:19:05 +0100 Subject: [PATCH 25/35] chore: Make the driver reload method public --- .../security/token/driver/TokenStoreDriver.java | 4 ++++ .../token/driver/file/FileTokenStoreDriver.java | 10 ++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java index 1e9e349..85fea96 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java @@ -9,6 +9,10 @@ public interface TokenStoreDriver extends AutoCloseable { + default void reload() { + CraftsNetSecurity.getTokenManager().clearCache(); + } + boolean exists(long id); Token load(long id); diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java index fd112d4..c9576fe 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java @@ -68,6 +68,8 @@ public FileTokenStoreDriver(@NotNull Path tokensFile) { Path path = (Path) event.context(); Path realPath = tokensDirectory.resolve(path); if (realPath.equals(tokensFile.toAbsolutePath())) { + CraftsNetSecurity.getInstance().getLogger().debug("Detected file system change, " + + "reloading token file."); this.reload(); } } @@ -82,15 +84,11 @@ public FileTokenStoreDriver(@NotNull Path tokensFile) { } } - private void reload() { + public void reload() { ensureOpen(); synchronized (this.tokens) { - if (this.tokens.get() != null) { - CraftsNetSecurity.getInstance().getLogger().debug("Detected file system change, " + - "reloading token file."); - } - this.tokens.set(JsonParser.parse(tokensFile)); + TokenStoreDriver.super.reload(); } } From f9038d7a22eb91b4fdcdfe21626ef7246ab3f330 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:59:52 +0100 Subject: [PATCH 26/35] chore: Delegate to super on file token deletion --- .../modules/security/token/driver/file/FileTokenStoreDriver.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java index c9576fe..ac5c2d3 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java @@ -132,6 +132,7 @@ public void delete(Token token) { ensureOpen(); synchronized (this.tokens) { this.tokens.get().remove(String.valueOf(token.id())); + TokenStoreDriver.super.delete(token); } } From 94229d3d0afdcd7c0784101e0a9cb1ce4a28a4ec Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:14:14 +0100 Subject: [PATCH 27/35] chore: Make the cache size configurable --- .../cnet/modules/security/token/TokenManager.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java b/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java index 7f2dbef..341eeba 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java @@ -15,7 +15,15 @@ public class TokenManager { - private final Cache tokenCache = new Cache<>(25); + private final Cache tokenCache; + + public TokenManager() { + this(25); + } + + public TokenManager(int cacheSize) { + this.tokenCache = new Cache<>(cacheSize); + } public void persist(Token token) { CraftsNetSecurity.getTokenStoreDriver().save(token); From 049ffb7fcfe65480918c7f22c438b300a28e709c Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:14:54 +0100 Subject: [PATCH 28/35] fix: Do not double load the token driver --- .../cnet/modules/security/token/TokenManager.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java b/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java index 341eeba..048c7c6 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java @@ -1,6 +1,7 @@ package de.craftsblock.cnet.modules.security.token; import de.craftsblock.cnet.modules.security.CraftsNetSecurity; +import de.craftsblock.cnet.modules.security.token.driver.TokenStoreDriver; import de.craftsblock.cnet.modules.security.token.event.TokenCreateEvent; import de.craftsblock.cnet.modules.security.token.util.NewToken; import de.craftsblock.cnet.modules.security.token.util.TokenParts; @@ -38,11 +39,12 @@ public Token getToken(long id) { return tokenCache.get(id); } - if (!CraftsNetSecurity.getTokenStoreDriver().exists(id)) { + TokenStoreDriver driver = CraftsNetSecurity.getTokenStoreDriver(); + if (!driver.exists(id)) { return null; } - Token token = CraftsNetSecurity.getTokenStoreDriver().load(id); + Token token = driver.load(id); tokenCache.put(token.id(), token); return token; } From 2f16e9671f783f3f3168601a95611c4f651b1ce2 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:22:42 +0100 Subject: [PATCH 29/35] feat: Add setter for the token manager --- .../modules/security/CraftsNetSecurity.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java b/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java index d5325ac..b9eb46f 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java @@ -2,13 +2,12 @@ import de.craftsblock.cnet.modules.security.auth.AuthChain; import de.craftsblock.cnet.modules.security.token.TokenManager; -import de.craftsblock.cnet.modules.security.token.adapter.HttpTokenAuthAdapter; -import de.craftsblock.cnet.modules.security.token.adapter.WebSocketTokenAuthAdapter; import de.craftsblock.cnet.modules.security.token.driver.TokenStoreDriver; import de.craftsblock.craftsnet.CraftsNet; import de.craftsblock.craftsnet.addon.Addon; import de.craftsblock.craftsnet.addon.meta.annotations.Meta; import de.craftsblock.craftsnet.builder.ActivateType; +import org.jetbrains.annotations.NotNull; import java.io.IOException; @@ -32,8 +31,6 @@ public static void main(String[] args) throws IOException { @Override public void onLoad() { this.authChain = new AuthChain(); - this.authChain.append(new HttpTokenAuthAdapter(null)); - this.authChain.append(new WebSocketTokenAuthAdapter()); this.tokenManager = new TokenManager(); } @@ -48,19 +45,23 @@ public void onDisable() { this.tokenStoreDriver.close(); } - public static AuthChain getAuthChain() { + public static @NotNull AuthChain getAuthChain() { return getInstance().authChain; } - public static TokenManager getTokenManager() { + public synchronized static void setTokenManager(@NotNull TokenManager tokenManager) { + getInstance().tokenManager = tokenManager; + } + + public synchronized static @NotNull TokenManager getTokenManager() { return getInstance().tokenManager; } - public static void setTokenStoreDriver(TokenStoreDriver tokenStoreDriver) { + public synchronized static void setTokenStoreDriver(@NotNull TokenStoreDriver tokenStoreDriver) { getInstance().tokenStoreDriver = tokenStoreDriver; } - public static TokenStoreDriver getTokenStoreDriver() { + public synchronized static TokenStoreDriver getTokenStoreDriver() { return getInstance().tokenStoreDriver; } From a81c476a711558e4f60c441df6266b117a6df59e Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:28:07 +0100 Subject: [PATCH 30/35] feat: A more generic way to register fallback driver --- .../adapter/WebSocketTokenAuthAdapter.java | 1 - .../listener/TokenPostSetupListener.java | 24 +++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java index f28cd04..56e8b23 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java @@ -20,7 +20,6 @@ import de.craftsblock.craftsnet.events.sockets.message.IncomingSocketMessageEvent; import de.craftsblock.craftsnet.events.sockets.message.OutgoingSocketMessageEvent; -@AutoRegister public class WebSocketTokenAuthAdapter implements ListenerAdapter, AuthAdapter.WebSocket { private static final String MESSAGE_LITERAL_WRONG_AUTH = "Not allowed!"; diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java b/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java index d313f97..cec3057 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java @@ -1,28 +1,42 @@ package de.craftsblock.cnet.modules.security.token.listener; import de.craftsblock.cnet.modules.security.CraftsNetSecurity; -import de.craftsblock.cnet.modules.security.token.driver.TokenStoreDriver; +import de.craftsblock.cnet.modules.security.token.driver.Driver; import de.craftsblock.cnet.modules.security.token.driver.file.FileTokenStoreDriver; import de.craftsblock.craftscore.event.EventHandler; import de.craftsblock.craftscore.event.EventPriority; import de.craftsblock.craftscore.event.ListenerAdapter; +import de.craftsblock.craftsnet.addon.meta.Startup; import de.craftsblock.craftsnet.autoregister.meta.AutoRegister; import de.craftsblock.craftsnet.events.addons.AllAddonsLoadedEvent; import java.nio.file.Path; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; -@AutoRegister +@AutoRegister(startup = Startup.LOAD) public class TokenPostSetupListener implements ListenerAdapter { @EventHandler(priority = EventPriority.HIGHEST) - public void handleAllAddonsLoaded(AllAddonsLoadedEvent event) { - TokenStoreDriver currentDriver = CraftsNetSecurity.getTokenStoreDriver(); + public void registerFallbackDriver(AllAddonsLoadedEvent event) { + registerFallbackDriver( + CraftsNetSecurity::getTokenStoreDriver, + CraftsNetSecurity::setTokenStoreDriver, + FileTokenStoreDriver::new, + "tokens.json" + ); + } + + private void registerFallbackDriver(Supplier current, Consumer setter, + Function initiator, String file) { + D currentDriver = current.get(); if (currentDriver != null) { return; } Path dataPath = CraftsNetSecurity.getInstance().getDataPath(); - CraftsNetSecurity.setTokenStoreDriver(new FileTokenStoreDriver(dataPath.resolve("tokens.json"))); + setter.accept(initiator.apply(dataPath.resolve(file))); } } From b5a39b5c905a10268c9c12885c65c53713224006 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:43:55 +0100 Subject: [PATCH 31/35] chore: No more auto register for tokens --- .../cnet/modules/security/token/driver/Driver.java | 8 ++++++++ .../modules/security/token/driver/TokenStoreDriver.java | 3 ++- .../modules/security/token/scope/ScopeRequirement.java | 4 ---- .../security/token/scope/ScopeResolveMiddleware.java | 4 +--- 4 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/driver/Driver.java diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/Driver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/Driver.java new file mode 100644 index 0000000..91e28eb --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/Driver.java @@ -0,0 +1,8 @@ +package de.craftsblock.cnet.modules.security.token.driver; + +public sealed interface Driver + permits TokenStoreDriver { + + void reload(); + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java index 85fea96..d8a7647 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java @@ -7,8 +7,9 @@ import java.util.Collection; -public interface TokenStoreDriver extends AutoCloseable { +public non-sealed interface TokenStoreDriver extends AutoCloseable, Driver { + @Override default void reload() { CraftsNetSecurity.getTokenManager().clearCache(); } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeRequirement.java b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeRequirement.java index 36bf237..26f70b6 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeRequirement.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeRequirement.java @@ -1,13 +1,11 @@ package de.craftsblock.cnet.modules.security.token.scope; -import de.craftsblock.craftsnet.addon.meta.Startup; import de.craftsblock.craftsnet.api.RouteRegistry; import de.craftsblock.craftsnet.api.http.Request; import de.craftsblock.craftsnet.api.requirements.web.WebRequirement; import de.craftsblock.craftsnet.api.requirements.websocket.WebSocketRequirement; import de.craftsblock.craftsnet.api.utils.Context; import de.craftsblock.craftsnet.api.websocket.WebSocketClient; -import de.craftsblock.craftsnet.autoregister.meta.AutoRegister; import org.jetbrains.annotations.ApiStatus; import java.lang.annotation.Annotation; @@ -32,7 +30,6 @@ default boolean injectRequest(Context context, RouteRegistry.EndpointMapping map Class getAnnotation(); @ApiStatus.Internal - @AutoRegister(startup = Startup.LOAD) final class Http extends WebRequirement implements ScopeRequirement { public Http() { @@ -47,7 +44,6 @@ public boolean applies(Request request, RouteRegistry.EndpointMapping mapping) { } @ApiStatus.Internal - @AutoRegister(startup = Startup.LOAD) final class WebSocket extends WebSocketRequirement implements ScopeRequirement { public WebSocket() { diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResolveMiddleware.java b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResolveMiddleware.java index 33546b0..00cab67 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResolveMiddleware.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/scope/ScopeResolveMiddleware.java @@ -6,13 +6,11 @@ import de.craftsblock.craftscore.event.EventPriority; import de.craftsblock.craftscore.event.ListenerAdapter; import de.craftsblock.craftscore.json.Json; -import de.craftsblock.craftsnet.addon.meta.Startup; import de.craftsblock.craftsnet.api.BaseExchange; import de.craftsblock.craftsnet.api.http.Exchange; import de.craftsblock.craftsnet.api.utils.Context; import de.craftsblock.craftsnet.api.websocket.ClosureCode; import de.craftsblock.craftsnet.api.websocket.SocketExchange; -import de.craftsblock.craftsnet.autoregister.meta.AutoRegister; import de.craftsblock.craftsnet.events.EventWithCancelReason; import de.craftsblock.craftsnet.events.requests.routes.RouteRequestEvent; import de.craftsblock.craftsnet.events.sockets.message.IncomingSocketMessageEvent; @@ -21,7 +19,6 @@ import java.util.function.Consumer; @ApiStatus.Internal -@AutoRegister(startup = Startup.LOAD) public class ScopeResolveMiddleware implements ListenerAdapter { private final Json MISSING_SCOPES_MESSAGE = Json.empty() @@ -41,6 +38,7 @@ private void handle(BaseExchange exchange, CancellableEvent event, T subject withCancelReason.setCancelReason("NO TOKEN"); } + onFailure.accept(subject); return; } From 7b455d4983545deb79ca9b40bd7598041f849acd Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:45:03 +0100 Subject: [PATCH 32/35] refactor: Split token driver into multiple untis --- .../driver/file/AbstractFileStoreDriver.java | 112 +++++++++++++ .../file/FileDriverHotReloadManager.java | 63 ++++++++ .../driver/file/FileTokenStoreDriver.java | 152 +++--------------- 3 files changed, 198 insertions(+), 129 deletions(-) create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/AbstractFileStoreDriver.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileDriverHotReloadManager.java diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/AbstractFileStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/AbstractFileStoreDriver.java new file mode 100644 index 0000000..cd73a0b --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/AbstractFileStoreDriver.java @@ -0,0 +1,112 @@ +package de.craftsblock.cnet.modules.security.token.driver.file; + +import de.craftsblock.cnet.modules.security.CraftsNetSecurity; +import de.craftsblock.craftscore.json.Json; +import de.craftsblock.craftscore.json.JsonParser; +import de.craftsblock.craftsnet.logging.Logger; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; + +abstract sealed class AbstractFileStoreDriver implements AutoCloseable + permits FileTokenStoreDriver { + + public static final int WARN_AT_FILE_SIZE = 1024 * 1024 * 15; + + final @NotNull Path file; + final @NotNull Path directory; + private final AtomicReference json = new AtomicReference<>(); + + private final @NotNull FileDriverHotReloadManager hotReloadManager; + private boolean closed = false; + + public AbstractFileStoreDriver(Path file) { + this.file = file; + this.directory = file.toAbsolutePath().getParent(); + + try { + if (Files.notExists(directory)) { + Files.createDirectories(directory); + } + + if (Files.notExists(file)) { + Files.createFile(file); + } + + long size = Files.size(file); + if (size >= WARN_AT_FILE_SIZE) { + Logger logger = CraftsNetSecurity.getInstance().getLogger(); + logger.warning( + "The store (%s) is larger than %s MB (%s MB), which may cause slowdowns!", + file, WARN_AT_FILE_SIZE / 1024 / 1024, size / 1024 / 1024 + ); + logger.warning("Please consider using a database."); + } + + this.reload(); + this.hotReloadManager = new FileDriverHotReloadManager(this); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read file: " + e.getMessage(), e); + } + } + + protected void json(Consumer consumer) { + ensureOpen(); + + synchronized (this.json) { + consumer.accept(this.json.get()); + } + } + + protected R json(Function function) { + ensureOpen(); + + synchronized (this.json) { + return function.apply(this.json.get()); + } + } + + public void reload() { + ensureOpen(); + synchronized (this.json) { + this.json.set(JsonParser.parse(file)); + } + } + + public void ensureOpen() { + if (closed) { + throw new IllegalStateException("No operations allowed after closure!"); + } + } + + @Override + public void close() { + ensureOpen(); + try { + this.hotReloadManager.close(); + this.json.set(null); + } finally { + this.closed = true; + } + } + + public @NotNull Path getFile() { + return file; + } + + public @NotNull Path getDirectory() { + return directory; + } + + public boolean isClosed() { + return closed; + } + + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileDriverHotReloadManager.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileDriverHotReloadManager.java new file mode 100644 index 0000000..c417f6d --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileDriverHotReloadManager.java @@ -0,0 +1,63 @@ +package de.craftsblock.cnet.modules.security.token.driver.file; + +import de.craftsblock.cnet.modules.security.CraftsNetSecurity; +import de.craftsblock.craftsnet.utils.reflection.TypeUtils; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.*; + +class FileDriverHotReloadManager extends Thread implements AutoCloseable { + + private final @NotNull AbstractFileStoreDriver driver; + private final @NotNull WatchService watchService; + + public FileDriverHotReloadManager(@NotNull AbstractFileStoreDriver driver) { + super("Token file watcher"); + try { + this.driver = driver; + this.watchService = FileSystems.getDefault().newWatchService(); + driver.getDirectory().register(this.watchService, StandardWatchEventKinds.ENTRY_MODIFY); + } catch (IOException e) { + throw new UncheckedIOException("Failed to create: " + getClass().getSimpleName(), e); + } + + this.start(); + } + + @Override + public void run() { + try { + WatchKey key; + while ((key = watchService.take()) != null) { + for (WatchEvent event : key.pollEvents()) { + if (!TypeUtils.isAssignable(Path.class, event.kind().type())) { + continue; + } + + Path path = (Path) event.context(); + Path realPath = driver.getDirectory().resolve(path); + if (realPath.equals(driver.getFile().toAbsolutePath())) { + CraftsNetSecurity.getInstance().getLogger().debug("Detected file system change, " + + "reloading token file."); + driver.reload(); + } + } + key.reset(); + } + } catch (InterruptedException ignored) { + } + } + + @Override + public void close() { + try { + this.watchService.close(); + this.interrupt(); + this.join(); + } catch (IOException | InterruptedException e) { + throw new RuntimeException("Failed to close: " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java index ac5c2d3..7f63f0c 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java @@ -1,113 +1,38 @@ package de.craftsblock.cnet.modules.security.token.driver.file; -import de.craftsblock.cnet.modules.security.CraftsNetSecurity; import de.craftsblock.cnet.modules.security.token.Token; import de.craftsblock.cnet.modules.security.token.driver.TokenStoreDriver; import de.craftsblock.craftscore.json.Json; -import de.craftsblock.craftscore.json.JsonParser; -import de.craftsblock.craftsnet.logging.Logger; -import de.craftsblock.craftsnet.utils.reflection.TypeUtils; import org.jetbrains.annotations.NotNull; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.*; +import java.nio.file.Path; import java.util.Collection; import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; -public class FileTokenStoreDriver implements TokenStoreDriver { - - public static final int WARN_AT_FILE_SIZE = 1024 * 1024 * 15; - - private final @NotNull Path tokensDirectory; - private final @NotNull Path tokensFile; - private final @NotNull AtomicReference tokens = new AtomicReference<>(); - - private final @NotNull Thread watchThread; - private final @NotNull WatchService watchService; - - private boolean closed = false; +public final class FileTokenStoreDriver extends AbstractFileStoreDriver implements TokenStoreDriver { public FileTokenStoreDriver(@NotNull Path tokensFile) { - this.tokensFile = tokensFile; - this.tokensDirectory = tokensFile.toAbsolutePath().getParent(); - - try { - if (Files.notExists(tokensDirectory)) { - Files.createDirectories(tokensDirectory); - } - - if (Files.notExists(tokensFile)) { - Files.createFile(tokensFile); - } - - long size = Files.size(tokensFile); - if (size >= WARN_AT_FILE_SIZE) { - Logger logger = CraftsNetSecurity.getInstance().getLogger(); - logger.warning( - "The token store is larger than %s MB (%s MB), which may cause slowdowns!", - WARN_AT_FILE_SIZE / 1024 / 1024, size / 1024 / 1024 - ); - logger.warning("Please consider using a database."); - } - - this.reload(); - this.watchService = FileSystems.getDefault().newWatchService(); - this.tokensDirectory.register(this.watchService, StandardWatchEventKinds.ENTRY_MODIFY); - - this.watchThread = new Thread(() -> { - try { - WatchKey key; - while ((key = watchService.take()) != null) { - for (WatchEvent event : key.pollEvents()) { - if (!TypeUtils.isAssignable(Path.class, event.kind().type())) { - continue; - } - - Path path = (Path) event.context(); - Path realPath = tokensDirectory.resolve(path); - if (realPath.equals(tokensFile.toAbsolutePath())) { - CraftsNetSecurity.getInstance().getLogger().debug("Detected file system change, " + - "reloading token file."); - this.reload(); - } - } - key.reset(); - } - } catch (InterruptedException ignored) { - } - }, "Token file watcher"); - this.watchThread.start(); - } catch (IOException e) { - throw new UncheckedIOException("Failed to read file: " + e.getMessage(), e); - } + super(tokensFile); } + @Override public void reload() { - ensureOpen(); - synchronized (this.tokens) { - this.tokens.set(JsonParser.parse(tokensFile)); - TokenStoreDriver.super.reload(); - } + TokenStoreDriver.super.reload(); + super.reload(); } @Override public boolean exists(long id) { - ensureOpen(); - synchronized (this.tokens) { - return this.tokens.get().contains(String.valueOf(id)); - } + return this.json(json -> { + return json.contains(String.valueOf(id)); + }); } @Override public Token load(long id) { - ensureOpen(); - Json token; - - synchronized (this.tokens) { - token = this.tokens.get().getJson(String.valueOf(id)); - } + Json token = this.json(json -> { + return json.getJson(String.valueOf(id)); + }); if (token == null) { throw new IllegalStateException("Token for id %s not found".formatted(id)); @@ -118,61 +43,30 @@ public Token load(long id) { @Override public void save(@NotNull Token token) { - ensureOpen(); - Json json = token.toJson(); - - synchronized (this.tokens) { - this.tokens.get().set(String.valueOf(token.id()), json); - this.tokens.get().save(tokensFile); - } + this.json(json -> { + json.set(String.valueOf(token.id()), token.toJson()); + json.save(file); + }); } @Override public void delete(Token token) { - ensureOpen(); - synchronized (this.tokens) { - this.tokens.get().remove(String.valueOf(token.id())); + this.json(json -> { + json.remove(String.valueOf(token.id())); + json.save(file); TokenStoreDriver.super.delete(token); - } + }); } @Override public Collection getAllTokenIds() { - ensureOpen(); - Set stringIds; - - synchronized (this.tokens) { - stringIds = this.tokens.get().keySet(); - } + Set stringIds = this.json(json -> { + return json.keySet(); + }); return stringIds.stream() .map(Long::parseLong) .toList(); } - public void ensureOpen() { - if (closed) { - throw new IllegalStateException("No operations allowed after closure!"); - } - } - - @Override - public void close() { - ensureOpen(); - try { - this.watchThread.interrupt(); - this.watchThread.join(); - } catch (InterruptedException ignored) { - } - - try { - this.watchService.close(); - } catch (IOException e) { - throw new UncheckedIOException("Failed to close: " + e.getMessage(), e); - } - - this.tokens.set(null); - this.closed = true; - } - } From 1e0d3e223e8d81939df6365ac68b0f419861972d Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:46:47 +0100 Subject: [PATCH 33/35] feat: Add preview for token groups --- .../modules/security/CraftsNetSecurity.java | 22 +++++ .../cnet/modules/security/token/Token.java | 25 +++++- .../modules/security/token/TokenManager.java | 30 +++++-- .../modules/security/token/driver/Driver.java | 2 +- .../token/driver/GroupStoreDriver.java | 31 +++++++ .../driver/file/AbstractFileStoreDriver.java | 2 +- .../driver/file/FileGroupStoreDriver.java | 65 +++++++++++++++ .../modules/security/token/group/Group.java | 75 +++++++++++++++++ .../security/token/group/GroupManager.java | 83 +++++++++++++++++++ .../security/token/group/OptionalGroup.java | 42 ++++++++++ .../listener/TokenPostSetupListener.java | 7 ++ .../security/token/util/TokenUtil.java | 1 - 12 files changed, 373 insertions(+), 12 deletions(-) create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/driver/GroupStoreDriver.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileGroupStoreDriver.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/group/Group.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupManager.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/group/OptionalGroup.java diff --git a/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java b/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java index b9eb46f..c4ebfc2 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java @@ -2,7 +2,9 @@ import de.craftsblock.cnet.modules.security.auth.AuthChain; import de.craftsblock.cnet.modules.security.token.TokenManager; +import de.craftsblock.cnet.modules.security.token.driver.GroupStoreDriver; import de.craftsblock.cnet.modules.security.token.driver.TokenStoreDriver; +import de.craftsblock.cnet.modules.security.token.group.GroupManager; import de.craftsblock.craftsnet.CraftsNet; import de.craftsblock.craftsnet.addon.Addon; import de.craftsblock.craftsnet.addon.meta.annotations.Meta; @@ -16,7 +18,10 @@ public class CraftsNetSecurity extends Addon { private AuthChain authChain; + private GroupManager groupManager; private TokenManager tokenManager; + + private GroupStoreDriver groupStoreDriver; private TokenStoreDriver tokenStoreDriver; public static void main(String[] args) throws IOException { @@ -31,6 +36,7 @@ public static void main(String[] args) throws IOException { @Override public void onLoad() { this.authChain = new AuthChain(); + this.groupManager = new GroupManager(); this.tokenManager = new TokenManager(); } @@ -49,6 +55,14 @@ public void onDisable() { return getInstance().authChain; } + public synchronized static void setGroupManager(@NotNull GroupManager groupManager) { + getInstance().groupManager = groupManager; + } + + public synchronized static @NotNull GroupManager getGroupManager() { + return getInstance().groupManager; + } + public synchronized static void setTokenManager(@NotNull TokenManager tokenManager) { getInstance().tokenManager = tokenManager; } @@ -57,6 +71,14 @@ public synchronized static void setTokenManager(@NotNull TokenManager tokenManag return getInstance().tokenManager; } + public synchronized static void setGroupStoreDriver(@NotNull GroupStoreDriver groupStoreDriver) { + getInstance().groupStoreDriver = groupStoreDriver; + } + + public synchronized static GroupStoreDriver getGroupStoreDriver() { + return getInstance().groupStoreDriver; + } + public synchronized static void setTokenStoreDriver(@NotNull TokenStoreDriver tokenStoreDriver) { getInstance().tokenStoreDriver = tokenStoreDriver; } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/Token.java b/src/main/java/de/craftsblock/cnet/modules/security/token/Token.java index 3250722..55f7954 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/Token.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/Token.java @@ -1,20 +1,34 @@ package de.craftsblock.cnet.modules.security.token; import com.google.gson.JsonObject; +import com.google.gson.internal.Streams; +import de.craftsblock.cnet.modules.security.CraftsNetSecurity; +import de.craftsblock.cnet.modules.security.token.driver.GroupStoreDriver; +import de.craftsblock.cnet.modules.security.token.group.GroupManager; +import de.craftsblock.cnet.modules.security.token.group.OptionalGroup; import de.craftsblock.craftscore.json.Json; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; import org.jetbrains.annotations.UnmodifiableView; import org.springframework.security.crypto.bcrypt.BCrypt; import java.util.*; import java.util.stream.IntStream; +import java.util.stream.Stream; -public record Token(long id, @NotNull String hash, @NotNull Collection scopes, @NotNull TokenDataContainer tokenDataContainer) { +public record Token(long id, @NotNull String hash, @NotNull @UnmodifiableView Collection scopes, + @NotNull @UnmodifiableView Collection groups, @NotNull TokenDataContainer tokenDataContainer) { - public Token(long id, @NotNull String hash, @NotNull Collection scopes, @NotNull TokenDataContainer tokenDataContainer) { + public Token(long id, @NotNull String hash, @NotNull Collection scopes, + @NotNull Collection groups, @NotNull TokenDataContainer tokenDataContainer) { this.id = id; this.hash = hash; - this.scopes = Collections.unmodifiableCollection(scopes); + this.groups = Collections.unmodifiableCollection(groups); + this.scopes = Stream.concat( + scopes.stream(), + groups.stream().filter(OptionalGroup::persisted) + .flatMap(group -> group.scopes().stream()) + ).distinct().toList(); this.tokenDataContainer = tokenDataContainer; } @@ -31,7 +45,8 @@ public Json toJson() { Json json = Json.empty() .set("id", this.id) .set("hash", this.hash) - .set("scopes", this.scopes); + .set("scopes", this.scopes) + .set("groups", this.groups.stream().map(OptionalGroup::name).toList()); Map serializedTokenDataContainer = this.tokenDataContainer.serializeToMap(); serializedTokenDataContainer.forEach((key, data) -> json.set( @@ -64,10 +79,12 @@ public static Token fromJson(Json json) { }); TokenDataContainer tokenDataContainer = new TokenDataContainer(serializedTokenDataContainer); + GroupManager groupManager = CraftsNetSecurity.getGroupManager(); return new Token( json.getLong("id"), json.getString("hash"), json.getStringList("scopes"), + OptionalGroup.fromList(json.getStringList("groups")), tokenDataContainer ); } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java b/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java index 048c7c6..d1c5de6 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java @@ -3,6 +3,8 @@ import de.craftsblock.cnet.modules.security.CraftsNetSecurity; import de.craftsblock.cnet.modules.security.token.driver.TokenStoreDriver; import de.craftsblock.cnet.modules.security.token.event.TokenCreateEvent; +import de.craftsblock.cnet.modules.security.token.group.Group; +import de.craftsblock.cnet.modules.security.token.group.OptionalGroup; import de.craftsblock.cnet.modules.security.token.util.NewToken; import de.craftsblock.cnet.modules.security.token.util.TokenParts; import de.craftsblock.cnet.modules.security.token.util.TokenUtil; @@ -12,6 +14,7 @@ import org.springframework.security.crypto.bcrypt.BCrypt; import java.util.Collection; +import java.util.Collections; import java.util.List; public class TokenManager { @@ -68,11 +71,19 @@ public Token getValidatedToken(String token) { } public NewToken newPersistedToken(String... scopes) { - return this.newPersistedToken(List.of(scopes)); + return this.newPersistedToken(List.of(scopes), Collections.emptyList()); } - public NewToken newPersistedToken(Collection scopes) { - NewToken newToken = newToken(scopes); + public NewToken newPersistedToken(String[] scopes, String... groups) { + return this.newPersistedToken(List.of(scopes), List.of(groups)); + } + + public NewToken newPersistedToken(Collection scopes, String... groups) { + return this.newPersistedToken(scopes, List.of(groups)); + } + + public NewToken newPersistedToken(Collection scopes, Collection groups) { + NewToken newToken = newToken(scopes, groups); persist(newToken.token()); return newToken; } @@ -81,13 +92,22 @@ public NewToken newToken(String... scopes) { return this.newToken(List.of(scopes)); } - public NewToken newToken(Collection scopes) { + public NewToken newToken(String[] scopes, String... groups) { + return this.newToken(List.of(scopes), List.of(groups)); + } + + public NewToken newToken(Collection scopes, String... groups) { + return this.newToken(scopes, List.of(groups)); + } + + public NewToken newToken(Collection scopes, Collection groups) { long id = Snowflake.generate(); byte[] secret = TokenUtil.newSecureSecret(); String secretHash = BCrypt.hashpw(secret, BCrypt.gensalt()); try { - Token token = new Token(id, secretHash, scopes, new TokenDataContainer()); + Token token = new Token(id, secretHash, scopes, OptionalGroup.fromList(groups), + new TokenDataContainer()); CraftsNetSecurity.getInstance().getListenerRegistry().call(new TokenCreateEvent(token)); return new NewToken( token, diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/Driver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/Driver.java index 91e28eb..38bafc3 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/Driver.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/Driver.java @@ -1,7 +1,7 @@ package de.craftsblock.cnet.modules.security.token.driver; public sealed interface Driver - permits TokenStoreDriver { + permits GroupStoreDriver, TokenStoreDriver { void reload(); diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/GroupStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/GroupStoreDriver.java new file mode 100644 index 0000000..1a859a8 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/GroupStoreDriver.java @@ -0,0 +1,31 @@ +package de.craftsblock.cnet.modules.security.token.driver; + +import de.craftsblock.cnet.modules.security.CraftsNetSecurity; +import de.craftsblock.cnet.modules.security.token.group.Group; + +import java.util.Collection; + +public non-sealed interface GroupStoreDriver extends AutoCloseable, Driver { + + default void reload() { + CraftsNetSecurity.getGroupManager().clearCache(); + } + + boolean exists(String name); + + Group load(String name); + + void save(Group token); + + default void delete(String name) { + this.delete(load(name)); + } + + void delete(Group group); + + Collection getAllGroupNames(); + + @Override + void close(); + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/AbstractFileStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/AbstractFileStoreDriver.java index cd73a0b..a51ae1f 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/AbstractFileStoreDriver.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/AbstractFileStoreDriver.java @@ -15,7 +15,7 @@ import java.util.function.Function; abstract sealed class AbstractFileStoreDriver implements AutoCloseable - permits FileTokenStoreDriver { + permits FileGroupStoreDriver, FileTokenStoreDriver { public static final int WARN_AT_FILE_SIZE = 1024 * 1024 * 15; diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileGroupStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileGroupStoreDriver.java new file mode 100644 index 0000000..e6ff37b --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileGroupStoreDriver.java @@ -0,0 +1,65 @@ +package de.craftsblock.cnet.modules.security.token.driver.file; + +import de.craftsblock.cnet.modules.security.token.driver.GroupStoreDriver; +import de.craftsblock.cnet.modules.security.token.group.Group; +import de.craftsblock.craftscore.json.Json; + +import java.nio.file.Path; +import java.util.Collection; + +public final class FileGroupStoreDriver extends AbstractFileStoreDriver implements GroupStoreDriver { + + public FileGroupStoreDriver(Path file) { + super(file); + } + + @Override + public void reload() { + GroupStoreDriver.super.reload(); + super.reload(); + } + + @Override + public boolean exists(String name) { + return this.json(json -> { + return json.contains(name); + }); + } + + @Override + public Group load(String name) { + Json group = this.json(json -> { + return json.getJson(name); + }); + + if (group == null) { + throw new IllegalStateException("Group for name %s not found".formatted(name)); + } + + return Group.fromJson(group); + } + + @Override + public void save(Group group) { + this.json(json -> { + json.set(group.name(), group.toJson()); + json.save(file); + }); + } + + @Override + public void delete(Group group) { + this.json(json -> { + json.remove(group.name()); + json.save(file); + }); + } + + @Override + public Collection getAllGroupNames() { + return this.json(json -> { + return json.keySet(); + }); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/group/Group.java b/src/main/java/de/craftsblock/cnet/modules/security/token/group/Group.java new file mode 100644 index 0000000..dd7cf7c --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/group/Group.java @@ -0,0 +1,75 @@ +package de.craftsblock.cnet.modules.security.token.group; + +import de.craftsblock.craftscore.json.Json; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.*; + +public final class Group { + + private final @NotNull String name; + private final @NotNull Collection scopes; + private final @NotNull + @UnmodifiableView Collection scopesView; + + public Group(@NotNull String name, @NotNull Collection scopes) { + this.name = name; + this.scopes = new ArrayList<>(scopes.stream().distinct().toList()); + this.scopesView = Collections.unmodifiableCollection(this.scopes); + } + + public void addScopes(String... scopes) { + for (String scope : scopes) { + if (!hasScope(scope)) { + this.scopes.add(scope); + } + } + } + + public void removeScopes(String... scopes) { + this.scopes.removeAll(Arrays.asList(scopes)); + } + + public boolean hasScope(String scope) { + return scopes.contains(scope); + } + + public boolean hasScopes(String... scopes) { + return this.scopes.containsAll(Arrays.asList(scopes)); + } + + public @NotNull String name() { + return name; + } + + public @NotNull @UnmodifiableView Collection scopes() { + return scopesView; + } + + public Json toJson() { + return Json.empty() + .set("name", name) + .set("scopes", scopes); + } + + public static Group fromJson(Json json) { + return new Group(json.getString("name"), json.getStringList("scopes")); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (Group) obj; + return Objects.equals(this.name, that.name) && + Objects.equals(this.scopes, that.scopes); + } + + @Override + public int hashCode() { + return Objects.hash(name, scopes); + } + + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupManager.java b/src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupManager.java new file mode 100644 index 0000000..2181a4e --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupManager.java @@ -0,0 +1,83 @@ +package de.craftsblock.cnet.modules.security.token.group; + +import de.craftsblock.cnet.modules.security.CraftsNetSecurity; +import de.craftsblock.cnet.modules.security.token.driver.GroupStoreDriver; +import de.craftsblock.craftscore.cache.Cache; + +import java.util.Arrays; +import java.util.function.Consumer; + +public class GroupManager { + + private final Cache groupCache; + + public GroupManager() { + this(25); + } + + public GroupManager(int cacheSize) { + this.groupCache = new Cache<>(cacheSize); + } + + public synchronized Group createOrUpdateGroup(String name, Consumer updater) { + GroupStoreDriver driver = CraftsNetSecurity.getGroupStoreDriver(); + if (driver.exists(name)) { + return this.updateGroup(name, updater); + } + + Group group = this.createGroup(name); + updater.accept(group); + driver.save(group); + return group; + } + + public synchronized Group createGroup(String name, String... scopes) { + GroupStoreDriver driver = CraftsNetSecurity.getGroupStoreDriver(); + Group existing = getGroup(name); + if (existing != null) { + return existing; + } + + Group group = new Group(name, Arrays.asList(scopes)); + driver.save(group); + groupCache.put(name, group); + return group; + } + + public synchronized Group updateGroup(String name, Consumer updater) { + GroupStoreDriver driver = CraftsNetSecurity.getGroupStoreDriver(); + Group group = getGroup(name); + if (group == null) { + return null; + } + + updater.accept(group); + driver.save(group); + return group; + } + + public synchronized Group getGroup(String name) { + if (groupCache.containsKey(name)) { + return groupCache.get(name); + } + + GroupStoreDriver driver = CraftsNetSecurity.getGroupStoreDriver(); + Group group = driver.load(name); + if (group == null) { + return null; + } + + groupCache.put(name, group); + return group; + } + + public synchronized void deleteGroup(String name) { + CraftsNetSecurity.getGroupStoreDriver().delete(name); + groupCache.remove(name); + } + + public synchronized void clearCache() { + groupCache.clear(); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/group/OptionalGroup.java b/src/main/java/de/craftsblock/cnet/modules/security/token/group/OptionalGroup.java new file mode 100644 index 0000000..73af63b --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/group/OptionalGroup.java @@ -0,0 +1,42 @@ +package de.craftsblock.cnet.modules.security.token.group; + +import de.craftsblock.cnet.modules.security.CraftsNetSecurity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; + +public record OptionalGroup(@NotNull String name, Optional optionalGroup) { + + public boolean persisted() { + return optionalGroup.isPresent(); + } + + public @Nullable Group group() { + return optionalGroup.orElse(null); + } + + public @NotNull @UnmodifiableView Collection scopes() { + Group group = group(); + if (group == null) { + return Collections.emptyList(); + } + + return group.scopes(); + } + + public static OptionalGroup fromString(String name) { + return new OptionalGroup( + name, + Optional.ofNullable(CraftsNetSecurity.getGroupManager().getGroup(name)) + ); + } + + public static Collection fromList(Collection names) { + return names.stream().map(OptionalGroup::fromString).toList(); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java b/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java index cec3057..605acf0 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java @@ -2,6 +2,7 @@ import de.craftsblock.cnet.modules.security.CraftsNetSecurity; import de.craftsblock.cnet.modules.security.token.driver.Driver; +import de.craftsblock.cnet.modules.security.token.driver.file.FileGroupStoreDriver; import de.craftsblock.cnet.modules.security.token.driver.file.FileTokenStoreDriver; import de.craftsblock.craftscore.event.EventHandler; import de.craftsblock.craftscore.event.EventPriority; @@ -26,6 +27,12 @@ public void registerFallbackDriver(AllAddonsLoadedEvent event) { FileTokenStoreDriver::new, "tokens.json" ); + registerFallbackDriver( + CraftsNetSecurity::getGroupStoreDriver, + CraftsNetSecurity::setGroupStoreDriver, + FileGroupStoreDriver::new, + "groups.json" + ); } private void registerFallbackDriver(Supplier current, Consumer setter, diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/util/TokenUtil.java b/src/main/java/de/craftsblock/cnet/modules/security/token/util/TokenUtil.java index 67ac0cd..ccfe800 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/util/TokenUtil.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/util/TokenUtil.java @@ -1,6 +1,5 @@ package de.craftsblock.cnet.modules.security.token.util; -import de.craftsblock.cnet.modules.security.token.Token; import de.craftsblock.craftscore.buffer.BufferUtil; import de.craftsblock.craftsnet.utils.PassphraseUtils; From 0b99ad959042abc185d115cdc0cf9a635fa2e9ce Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:47:17 +0100 Subject: [PATCH 34/35] feat: Add group requirement preview --- .../security/token/group/GroupRequest.java | 9 ++ .../token/group/GroupRequirement.java | 57 +++++++++++++ .../token/group/GroupResolveMiddleware.java | 82 +++++++++++++++++++ .../security/token/group/RequireGroup.java | 18 ++++ 4 files changed, 166 insertions(+) create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupRequest.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupRequirement.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupResolveMiddleware.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/group/RequireGroup.java diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupRequest.java b/src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupRequest.java new file mode 100644 index 0000000..1fce104 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupRequest.java @@ -0,0 +1,9 @@ +package de.craftsblock.cnet.modules.security.token.group; + +import org.jetbrains.annotations.ApiStatus; + +import java.util.List; + +@ApiStatus.Internal +record GroupRequest(List groups) { +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupRequirement.java b/src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupRequirement.java new file mode 100644 index 0000000..2ea5aed --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupRequirement.java @@ -0,0 +1,57 @@ +package de.craftsblock.cnet.modules.security.token.group; + +import de.craftsblock.craftsnet.api.RouteRegistry; +import de.craftsblock.craftsnet.api.http.Request; +import de.craftsblock.craftsnet.api.requirements.web.WebRequirement; +import de.craftsblock.craftsnet.api.requirements.websocket.WebSocketRequirement; +import de.craftsblock.craftsnet.api.utils.Context; +import de.craftsblock.craftsnet.api.websocket.WebSocketClient; +import org.jetbrains.annotations.ApiStatus; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.List; + +@ApiStatus.Internal +public sealed interface GroupRequirement + permits GroupRequirement.Http, GroupRequirement.WebSocket { + + default boolean injectRequest(Context context, RouteRegistry.EndpointMapping mapping) { + if (mapping.isPresent(getAnnotation(), "value")) { + List groups = mapping.getRequirements(getAnnotation(), "value"); + context.put(new GroupRequest(groups)); + } else { + context.put(new GroupRequest(Collections.emptyList())); + } + + return true; + } + + Class getAnnotation(); + + final class Http extends WebRequirement implements GroupRequirement { + + public Http() { + super(RequireGroup.class); + } + + @Override + public boolean applies(Request request, RouteRegistry.EndpointMapping mapping) { + return injectRequest(request.getExchange().context(), mapping); + } + + } + + final class WebSocket extends WebSocketRequirement implements GroupRequirement { + + public WebSocket() { + super(RequireGroup.class); + } + + @Override + public boolean applies(WebSocketClient client, RouteRegistry.EndpointMapping mapping) { + return injectRequest(client.getContext(), mapping); + } + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupResolveMiddleware.java b/src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupResolveMiddleware.java new file mode 100644 index 0000000..bd29196 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupResolveMiddleware.java @@ -0,0 +1,82 @@ +package de.craftsblock.cnet.modules.security.token.group; + +import de.craftsblock.cnet.modules.security.token.Token; +import de.craftsblock.craftscore.event.CancellableEvent; +import de.craftsblock.craftscore.event.EventHandler; +import de.craftsblock.craftscore.event.EventPriority; +import de.craftsblock.craftscore.event.ListenerAdapter; +import de.craftsblock.craftscore.json.Json; +import de.craftsblock.craftsnet.api.BaseExchange; +import de.craftsblock.craftsnet.api.http.Exchange; +import de.craftsblock.craftsnet.api.utils.Context; +import de.craftsblock.craftsnet.api.websocket.ClosureCode; +import de.craftsblock.craftsnet.api.websocket.SocketExchange; +import de.craftsblock.craftsnet.events.EventWithCancelReason; +import de.craftsblock.craftsnet.events.requests.routes.RouteRequestEvent; +import de.craftsblock.craftsnet.events.sockets.message.IncomingSocketMessageEvent; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.Consumer; + +@ApiStatus.Internal +public class GroupResolveMiddleware implements ListenerAdapter { + + private final Json MISSING_GROUPS_MESSAGE = Json.empty() + .set("success", false) + .set("error.code", 400) + .set("error.message", "Not allowed!"); + + private void handle(BaseExchange exchange, CancellableEvent event, T subject, Consumer onFailure) { + Context context = exchange.context(); + if (context == null || !context.containsKey(GroupRequest.class)) { + return; + } + + if (!context.containsKey(Token.class)) { + event.setCancelled(true); + if (event instanceof EventWithCancelReason withCancelReason) { + withCancelReason.setCancelReason("NO TOKEN"); + } + + onFailure.accept(subject); + return; + } + + final Token token = context.getTyped(Token.class); + final GroupRequest result = context.getTyped(GroupRequest.class); + if (token.groups().containsAll(result.groups())) { + return; + } + + event.setCancelled(true); + if (event instanceof EventWithCancelReason withCancelReason) { + withCancelReason.setCancelReason("GROUP MISMATCH"); + } + + onFailure.accept(subject); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreWhenCancelled = true) + public void handleRequest(RouteRequestEvent event) { + final Exchange exchange = event.getExchange(); + handle(exchange, event, exchange.response(), response -> { + if (!response.headersSent()) { + response.setCode(400); + } + + if (!response.sendingFile()) { + response.print(MISSING_GROUPS_MESSAGE); + } + }); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreWhenCancelled = true) + public void handleWebSocketMessage(IncomingSocketMessageEvent event) { + final SocketExchange exchange = event.getExchange(); + handle(exchange, event, exchange.client(), client -> { + client.sendMessage(MISSING_GROUPS_MESSAGE); + client.close(ClosureCode.NORMAL, "Not allowed!"); + }); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/group/RequireGroup.java b/src/main/java/de/craftsblock/cnet/modules/security/token/group/RequireGroup.java new file mode 100644 index 0000000..70ac6dc --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/group/RequireGroup.java @@ -0,0 +1,18 @@ +package de.craftsblock.cnet.modules.security.token.group; + +import de.craftsblock.craftsnet.api.requirements.meta.RequirementMeta; +import de.craftsblock.craftsnet.api.requirements.meta.RequirementStore; +import de.craftsblock.craftsnet.api.requirements.meta.RequirementType; + +import java.lang.annotation.*; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +@RequirementMeta(type = RequirementType.STORING) +public @interface RequireGroup { + + @RequirementStore + String[] value(); + +} From 3ae52ea9cca2783c5017cb2a9487f91c33310058 Mon Sep 17 00:00:00 2001 From: Philipp <62473021+CrAfTsArMy@users.noreply.github.com> Date: Sun, 1 Mar 2026 11:35:37 +0100 Subject: [PATCH 35/35] feat: Both drivers are now one unit --- .../modules/security/CraftsNetSecurity.java | 24 ++---- .../modules/security/token/TokenManager.java | 11 ++- .../adapter/WebSocketTokenAuthAdapter.java | 1 - .../modules/security/token/driver/Driver.java | 5 +- .../token/driver/GroupStoreDriver.java | 17 ++-- .../security/token/driver/StoreDriver.java | 22 +++++ .../token/driver/TokenStoreDriver.java | 18 ++-- .../token/driver/WrappedStoreDriver.java | 84 +++++++++++++++++++ .../file/FileDriverHotReloadManager.java | 2 +- .../driver/file/FileGroupStoreDriver.java | 10 +-- .../token/driver/file/FileStoreDriver.java | 30 +++++++ .../driver/file/FileTokenStoreDriver.java | 13 +-- .../security/token/group/GroupManager.java | 20 ++--- .../listener/TokenPostSetupListener.java | 33 ++------ 14 files changed, 197 insertions(+), 93 deletions(-) create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/driver/StoreDriver.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/driver/WrappedStoreDriver.java create mode 100644 src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileStoreDriver.java diff --git a/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java b/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java index c4ebfc2..3fc3ab4 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/CraftsNetSecurity.java @@ -2,8 +2,7 @@ import de.craftsblock.cnet.modules.security.auth.AuthChain; import de.craftsblock.cnet.modules.security.token.TokenManager; -import de.craftsblock.cnet.modules.security.token.driver.GroupStoreDriver; -import de.craftsblock.cnet.modules.security.token.driver.TokenStoreDriver; +import de.craftsblock.cnet.modules.security.token.driver.StoreDriver; import de.craftsblock.cnet.modules.security.token.group.GroupManager; import de.craftsblock.craftsnet.CraftsNet; import de.craftsblock.craftsnet.addon.Addon; @@ -21,8 +20,7 @@ public class CraftsNetSecurity extends Addon { private GroupManager groupManager; private TokenManager tokenManager; - private GroupStoreDriver groupStoreDriver; - private TokenStoreDriver tokenStoreDriver; + private StoreDriver storeDriver; public static void main(String[] args) throws IOException { CraftsNet.create(CraftsNetSecurity.class) @@ -48,7 +46,7 @@ public void onEnable() { @Override public void onDisable() { super.onDisable(); - this.tokenStoreDriver.close(); + this.storeDriver.close(); } public static @NotNull AuthChain getAuthChain() { @@ -71,20 +69,12 @@ public synchronized static void setTokenManager(@NotNull TokenManager tokenManag return getInstance().tokenManager; } - public synchronized static void setGroupStoreDriver(@NotNull GroupStoreDriver groupStoreDriver) { - getInstance().groupStoreDriver = groupStoreDriver; + public synchronized static void setStoreDriver(@NotNull StoreDriver storeDriver) { + getInstance().storeDriver = storeDriver; } - public synchronized static GroupStoreDriver getGroupStoreDriver() { - return getInstance().groupStoreDriver; - } - - public synchronized static void setTokenStoreDriver(@NotNull TokenStoreDriver tokenStoreDriver) { - getInstance().tokenStoreDriver = tokenStoreDriver; - } - - public synchronized static TokenStoreDriver getTokenStoreDriver() { - return getInstance().tokenStoreDriver; + public synchronized static StoreDriver getStoreDriver() { + return getInstance().storeDriver; } public static CraftsNetSecurity getInstance() { diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java b/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java index d1c5de6..37c53b6 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/TokenManager.java @@ -3,7 +3,6 @@ import de.craftsblock.cnet.modules.security.CraftsNetSecurity; import de.craftsblock.cnet.modules.security.token.driver.TokenStoreDriver; import de.craftsblock.cnet.modules.security.token.event.TokenCreateEvent; -import de.craftsblock.cnet.modules.security.token.group.Group; import de.craftsblock.cnet.modules.security.token.group.OptionalGroup; import de.craftsblock.cnet.modules.security.token.util.NewToken; import de.craftsblock.cnet.modules.security.token.util.TokenParts; @@ -30,11 +29,11 @@ public TokenManager(int cacheSize) { } public void persist(Token token) { - CraftsNetSecurity.getTokenStoreDriver().save(token); + CraftsNetSecurity.getStoreDriver().saveToken(token); } public void delete(Token token) { - CraftsNetSecurity.getTokenStoreDriver().delete(token); + CraftsNetSecurity.getStoreDriver().deleteToken(token); } public Token getToken(long id) { @@ -42,12 +41,12 @@ public Token getToken(long id) { return tokenCache.get(id); } - TokenStoreDriver driver = CraftsNetSecurity.getTokenStoreDriver(); - if (!driver.exists(id)) { + TokenStoreDriver driver = CraftsNetSecurity.getStoreDriver(); + if (!driver.existsToken(id)) { return null; } - Token token = driver.load(id); + Token token = driver.loadToken(id); tokenCache.put(token.id(), token); return token; } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java index 56e8b23..7489bbe 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/adapter/WebSocketTokenAuthAdapter.java @@ -16,7 +16,6 @@ import de.craftsblock.craftsnet.api.websocket.Opcode; import de.craftsblock.craftsnet.api.websocket.SocketExchange; import de.craftsblock.craftsnet.api.websocket.WebSocketClient; -import de.craftsblock.craftsnet.autoregister.meta.AutoRegister; import de.craftsblock.craftsnet.events.sockets.message.IncomingSocketMessageEvent; import de.craftsblock.craftsnet.events.sockets.message.OutgoingSocketMessageEvent; diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/Driver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/Driver.java index 38bafc3..5c3b2a3 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/Driver.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/Driver.java @@ -1,8 +1,9 @@ package de.craftsblock.cnet.modules.security.token.driver; -public sealed interface Driver +sealed interface Driver extends AutoCloseable permits GroupStoreDriver, TokenStoreDriver { - void reload(); + @Override + void close(); } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/GroupStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/GroupStoreDriver.java index 1a859a8..6fa5b35 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/GroupStoreDriver.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/GroupStoreDriver.java @@ -5,27 +5,24 @@ import java.util.Collection; -public non-sealed interface GroupStoreDriver extends AutoCloseable, Driver { +public non-sealed interface GroupStoreDriver extends Driver { default void reload() { CraftsNetSecurity.getGroupManager().clearCache(); } - boolean exists(String name); + boolean existsGroup(String name); - Group load(String name); + Group loadGroup(String name); - void save(Group token); + void saveGroup(Group group); - default void delete(String name) { - this.delete(load(name)); + default void deleteGroup(String name) { + this.deleteGroup(loadGroup(name)); } - void delete(Group group); + void deleteGroup(Group group); Collection getAllGroupNames(); - @Override - void close(); - } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/StoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/StoreDriver.java new file mode 100644 index 0000000..1f67935 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/StoreDriver.java @@ -0,0 +1,22 @@ +package de.craftsblock.cnet.modules.security.token.driver; + +public interface StoreDriver + extends AutoCloseable, GroupStoreDriver, TokenStoreDriver { + + @Override + void close(); + + default void reload() { + GroupStoreDriver.super.reload(); + TokenStoreDriver.super.reload(); + } + + default GroupStoreDriver getGroupStoreDriver() { + return this; + } + + default TokenStoreDriver getTokenStoreDriver() { + return this; + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java index d8a7647..e312fb6 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/TokenStoreDriver.java @@ -7,32 +7,28 @@ import java.util.Collection; -public non-sealed interface TokenStoreDriver extends AutoCloseable, Driver { +public non-sealed interface TokenStoreDriver extends Driver { - @Override default void reload() { CraftsNetSecurity.getTokenManager().clearCache(); } - boolean exists(long id); + boolean existsToken(long id); - Token load(long id); + Token loadToken(long id); - default void save(Token token) { + default void saveToken(Token token) { CraftsNetSecurity.getInstance().getListenerRegistry().call(new TokenPersistEvent(token)); } - default void delete(long id) { - this.delete(load(id)); + default void deleteToken(long id) { + this.deleteToken(loadToken(id)); } - default void delete(Token token) { + default void deleteToken(Token token) { CraftsNetSecurity.getInstance().getListenerRegistry().call(new TokenDeleteEvent(token)); } Collection getAllTokenIds(); - @Override - void close(); - } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/WrappedStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/WrappedStoreDriver.java new file mode 100644 index 0000000..9f06443 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/WrappedStoreDriver.java @@ -0,0 +1,84 @@ +package de.craftsblock.cnet.modules.security.token.driver; + +import de.craftsblock.cnet.modules.security.token.Token; +import de.craftsblock.cnet.modules.security.token.group.Group; + +import java.util.Collection; + +public class WrappedStoreDriver implements StoreDriver { + + private final G groupStoreDriver; + private final T tokenStoreDriver; + + public WrappedStoreDriver(G groupStoreDriver, T tokenStoreDriver) { + this.groupStoreDriver = groupStoreDriver; + this.tokenStoreDriver = tokenStoreDriver; + } + + @Override + public void close() { + this.groupStoreDriver.close(); + this.tokenStoreDriver.close(); + } + + @Override + public boolean existsGroup(String name) { + return this.groupStoreDriver.existsGroup(name); + } + + @Override + public Group loadGroup(String name) { + return this.groupStoreDriver.loadGroup(name); + } + + @Override + public void saveGroup(Group group) { + this.groupStoreDriver.saveGroup(group); + } + + @Override + public void deleteGroup(Group group) { + this.groupStoreDriver.deleteGroup(group); + } + + @Override + public Collection getAllGroupNames() { + return this.groupStoreDriver.getAllGroupNames(); + } + + @Override + public boolean existsToken(long id) { + return this.tokenStoreDriver.existsToken(id); + } + + @Override + public Token loadToken(long id) { + return this.tokenStoreDriver.loadToken(id); + } + + @Override + public void saveToken(Token token) { + this.tokenStoreDriver.saveToken(token); + } + + @Override + public void deleteToken(Token token) { + this.tokenStoreDriver.deleteToken(token); + } + + @Override + public Collection getAllTokenIds() { + return this.tokenStoreDriver.getAllTokenIds(); + } + + @Override + public G getGroupStoreDriver() { + return groupStoreDriver; + } + + @Override + public T getTokenStoreDriver() { + return tokenStoreDriver; + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileDriverHotReloadManager.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileDriverHotReloadManager.java index c417f6d..4fb7770 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileDriverHotReloadManager.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileDriverHotReloadManager.java @@ -46,7 +46,7 @@ public void run() { } key.reset(); } - } catch (InterruptedException ignored) { + } catch (InterruptedException | ClosedWatchServiceException ignored) { } } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileGroupStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileGroupStoreDriver.java index e6ff37b..98a0a2f 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileGroupStoreDriver.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileGroupStoreDriver.java @@ -9,7 +9,7 @@ public final class FileGroupStoreDriver extends AbstractFileStoreDriver implements GroupStoreDriver { - public FileGroupStoreDriver(Path file) { + FileGroupStoreDriver(Path file) { super(file); } @@ -20,14 +20,14 @@ public void reload() { } @Override - public boolean exists(String name) { + public boolean existsGroup(String name) { return this.json(json -> { return json.contains(name); }); } @Override - public Group load(String name) { + public Group loadGroup(String name) { Json group = this.json(json -> { return json.getJson(name); }); @@ -40,7 +40,7 @@ public Group load(String name) { } @Override - public void save(Group group) { + public void saveGroup(Group group) { this.json(json -> { json.set(group.name(), group.toJson()); json.save(file); @@ -48,7 +48,7 @@ public void save(Group group) { } @Override - public void delete(Group group) { + public void deleteGroup(Group group) { this.json(json -> { json.remove(group.name()); json.save(file); diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileStoreDriver.java new file mode 100644 index 0000000..eb05f86 --- /dev/null +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileStoreDriver.java @@ -0,0 +1,30 @@ +package de.craftsblock.cnet.modules.security.token.driver.file; + +import de.craftsblock.cnet.modules.security.token.driver.WrappedStoreDriver; + +import java.nio.file.Path; + +public final class FileStoreDriver extends WrappedStoreDriver { + + FileStoreDriver(FileGroupStoreDriver groupStoreDriver, FileTokenStoreDriver tokenStoreDriver) { + super(groupStoreDriver, tokenStoreDriver); + } + + @Override + public FileGroupStoreDriver getGroupStoreDriver() { + return super.getGroupStoreDriver(); + } + + @Override + public FileTokenStoreDriver getTokenStoreDriver() { + return super.getTokenStoreDriver(); + } + + public static FileStoreDriver create(Path groupsFile, Path tokensFile) { + return new FileStoreDriver( + new FileGroupStoreDriver(groupsFile), + new FileTokenStoreDriver(tokensFile) + ); + } + +} diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java index 7f63f0c..ffbf1de 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/driver/file/FileTokenStoreDriver.java @@ -11,7 +11,7 @@ public final class FileTokenStoreDriver extends AbstractFileStoreDriver implements TokenStoreDriver { - public FileTokenStoreDriver(@NotNull Path tokensFile) { + FileTokenStoreDriver(@NotNull Path tokensFile) { super(tokensFile); } @@ -22,14 +22,14 @@ public void reload() { } @Override - public boolean exists(long id) { + public boolean existsToken(long id) { return this.json(json -> { return json.contains(String.valueOf(id)); }); } @Override - public Token load(long id) { + public Token loadToken(long id) { Json token = this.json(json -> { return json.getJson(String.valueOf(id)); }); @@ -42,19 +42,20 @@ public Token load(long id) { } @Override - public void save(@NotNull Token token) { + public void saveToken(@NotNull Token token) { this.json(json -> { json.set(String.valueOf(token.id()), token.toJson()); json.save(file); + TokenStoreDriver.super.saveToken(token); }); } @Override - public void delete(Token token) { + public void deleteToken(Token token) { this.json(json -> { json.remove(String.valueOf(token.id())); json.save(file); - TokenStoreDriver.super.delete(token); + TokenStoreDriver.super.deleteToken(token); }); } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupManager.java b/src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupManager.java index 2181a4e..7383122 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupManager.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/group/GroupManager.java @@ -20,39 +20,39 @@ public GroupManager(int cacheSize) { } public synchronized Group createOrUpdateGroup(String name, Consumer updater) { - GroupStoreDriver driver = CraftsNetSecurity.getGroupStoreDriver(); - if (driver.exists(name)) { + GroupStoreDriver driver = CraftsNetSecurity.getStoreDriver(); + if (driver.existsGroup(name)) { return this.updateGroup(name, updater); } Group group = this.createGroup(name); updater.accept(group); - driver.save(group); + driver.saveGroup(group); return group; } public synchronized Group createGroup(String name, String... scopes) { - GroupStoreDriver driver = CraftsNetSecurity.getGroupStoreDriver(); + GroupStoreDriver driver = CraftsNetSecurity.getStoreDriver(); Group existing = getGroup(name); if (existing != null) { return existing; } Group group = new Group(name, Arrays.asList(scopes)); - driver.save(group); + driver.saveGroup(group); groupCache.put(name, group); return group; } public synchronized Group updateGroup(String name, Consumer updater) { - GroupStoreDriver driver = CraftsNetSecurity.getGroupStoreDriver(); + GroupStoreDriver driver = CraftsNetSecurity.getStoreDriver(); Group group = getGroup(name); if (group == null) { return null; } updater.accept(group); - driver.save(group); + driver.saveGroup(group); return group; } @@ -61,8 +61,8 @@ public synchronized Group getGroup(String name) { return groupCache.get(name); } - GroupStoreDriver driver = CraftsNetSecurity.getGroupStoreDriver(); - Group group = driver.load(name); + GroupStoreDriver driver = CraftsNetSecurity.getStoreDriver(); + Group group = driver.loadGroup(name); if (group == null) { return null; } @@ -72,7 +72,7 @@ public synchronized Group getGroup(String name) { } public synchronized void deleteGroup(String name) { - CraftsNetSecurity.getGroupStoreDriver().delete(name); + CraftsNetSecurity.getStoreDriver().deleteGroup(name); groupCache.remove(name); } diff --git a/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java b/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java index 605acf0..f7de64a 100644 --- a/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java +++ b/src/main/java/de/craftsblock/cnet/modules/security/token/listener/TokenPostSetupListener.java @@ -1,9 +1,8 @@ package de.craftsblock.cnet.modules.security.token.listener; import de.craftsblock.cnet.modules.security.CraftsNetSecurity; -import de.craftsblock.cnet.modules.security.token.driver.Driver; -import de.craftsblock.cnet.modules.security.token.driver.file.FileGroupStoreDriver; -import de.craftsblock.cnet.modules.security.token.driver.file.FileTokenStoreDriver; +import de.craftsblock.cnet.modules.security.token.driver.StoreDriver; +import de.craftsblock.cnet.modules.security.token.driver.file.FileStoreDriver; import de.craftsblock.craftscore.event.EventHandler; import de.craftsblock.craftscore.event.EventPriority; import de.craftsblock.craftscore.event.ListenerAdapter; @@ -12,38 +11,24 @@ import de.craftsblock.craftsnet.events.addons.AllAddonsLoadedEvent; import java.nio.file.Path; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; @AutoRegister(startup = Startup.LOAD) public class TokenPostSetupListener implements ListenerAdapter { @EventHandler(priority = EventPriority.HIGHEST) public void registerFallbackDriver(AllAddonsLoadedEvent event) { - registerFallbackDriver( - CraftsNetSecurity::getTokenStoreDriver, - CraftsNetSecurity::setTokenStoreDriver, - FileTokenStoreDriver::new, - "tokens.json" - ); - registerFallbackDriver( - CraftsNetSecurity::getGroupStoreDriver, - CraftsNetSecurity::setGroupStoreDriver, - FileGroupStoreDriver::new, - "groups.json" - ); - } - - private void registerFallbackDriver(Supplier current, Consumer setter, - Function initiator, String file) { - D currentDriver = current.get(); + StoreDriver currentDriver = CraftsNetSecurity.getStoreDriver(); if (currentDriver != null) { return; } Path dataPath = CraftsNetSecurity.getInstance().getDataPath(); - setter.accept(initiator.apply(dataPath.resolve(file))); + FileStoreDriver driver = FileStoreDriver.create( + dataPath.resolve("groups.json"), + dataPath.resolve("tokens.json") + ); + + CraftsNetSecurity.setStoreDriver(driver); } }