Introduce authentication framework with AuthConfig, AuthGate, and Authenticator classes, alongside comprehensive tests for rules, modes, and schemes.
CI - Test, Publish and Release / run-tests (push) Successful in 18s
CI - Test, Publish and Release / create-release (push) Successful in 20s
CI - Test, Publish and Release / check-and-publish (push) Successful in 18s

This commit is contained in:
CodingPhoenixx
2026-05-29 13:22:31 +02:00
parent d9b639a539
commit bcf5572aeb
39 changed files with 2629 additions and 326 deletions
@@ -1,7 +1,7 @@
package dev.coph.nextusweb.server.ratelimit;
import dev.coph.nextusweb.server.router.Request;
import dev.coph.nextusweb.server.router.Response;
import io.netty.handler.codec.http.HttpRequest;
import java.util.List;
import java.util.concurrent.Executors;
@@ -22,19 +22,38 @@ import java.util.concurrent.TimeUnit;
*/
public final class RateLimitGate {
/** Default idle age after which per-key limiter state is eligible for eviction. */
private static final long DEFAULT_STALE_AFTER_NANOS = 10L * 60 * 1_000_000_000L;
/** The rule set this gate enforces. */
private final RateLimitConfig config;
/** Idle age (nanoseconds) after which a limiter's per-key state may be evicted. */
private final long staleAfterNanos;
/** Single-threaded scheduler driving periodic cleanup of stale buckets. */
private final ScheduledExecutorService cleanup;
/**
* Creates a gate for the given configuration and starts a background cleanup task that runs
* every five minutes on a daemon thread.
* every five minutes on a daemon thread, evicting per-key state idle for more than ten
* minutes.
*
* @param config the rate-limit rules to enforce
*/
public RateLimitGate(RateLimitConfig config) {
this(config, DEFAULT_STALE_AFTER_NANOS);
}
/**
* Creates a gate with an explicit idle age before per-key limiter state is evicted.
*
* @param config the rate-limit rules to enforce
* @param staleAfterNanos idle age in nanoseconds after which per-key state is evicted; must
* be positive
*/
public RateLimitGate(RateLimitConfig config, long staleAfterNanos) {
if (staleAfterNanos <= 0) throw new IllegalArgumentException("staleAfterNanos must be > 0");
this.config = config;
this.staleAfterNanos = staleAfterNanos;
this.cleanup = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "ratelimit-cleanup");
t.setDaemon(true);
@@ -52,12 +71,13 @@ public final class RateLimitGate {
* independent. The first denial short-circuits and is returned immediately; if every rule
* allows the request, the result with the least remaining quota is returned.</p>
*
* @param req the incoming request, used by key resolvers
* @param path the request path used to select rules
* @param remoteAddress the client's remote address, used as a key-resolver fallback
* @param req the incoming request, used by key resolvers
* @param path the request path used to select rules
* @param clientIp the resolved client IP (honouring trusted proxies), used as a key-resolver
* fallback
* @return the limiting result, or {@code null} if no rule applies to the path
*/
public RateLimiter.Result check(HttpRequest req, String path, String remoteAddress) {
public RateLimiter.Result check(Request req, String path, String clientIp) {
List<RateLimitConfig.Rule> rules = config.rulesFor(path);
if (rules.isEmpty()) return null;
@@ -65,7 +85,7 @@ public final class RateLimitGate {
RateLimiter.Result strictest = null;
for (var rule : rules) {
String key = rule.name() + ":" + rule.keyResolver().resolve(req, remoteAddress);
String key = rule.name() + ":" + rule.keyResolver().resolve(req, clientIp);
RateLimiter.Result result = rule.limiter().tryAcquire(key, now);
if (!result.allowed()) return result;
@@ -97,11 +117,18 @@ public final class RateLimitGate {
}
/**
* Periodic cleanup hook invoked by the background scheduler to evict limiter state that has
* not been touched recently (older than roughly ten minutes).
* Periodic cleanup hook invoked by the background scheduler. Asks every configured limiter to
* evict per-key state idle for longer than {@link #staleAfterNanos}. A failure cleaning one
* limiter must not abort the others or kill the scheduler, so each call is guarded.
*/
private void doCleanup() {
long threshold = 10L * 60 * 1_000_000_000L;
for (RateLimiter limiter : config.allLimiters()) {
try {
limiter.cleanup(staleAfterNanos);
} catch (RuntimeException ignored) {
// Best-effort eviction; never let one limiter break the cleanup cycle.
}
}
}
/**