198 lines
6.8 KiB
Java
198 lines
6.8 KiB
Java
package dev.coph.nextusweb.server.ratelimit;
|
|
|
|
import java.util.*;
|
|
|
|
/**
|
|
* Immutable mapping from request paths to the {@link Rule rate-limit rules} that apply to them.
|
|
*
|
|
* <p>Three kinds of rules can be configured, resolved with the following precedence by
|
|
* {@link #rulesFor(String)}:</p>
|
|
* <ol>
|
|
* <li>an optional <strong>global</strong> rule that applies to every request;</li>
|
|
* <li><strong>exact-path</strong> rules matched by exact path equality;</li>
|
|
* <li><strong>prefix</strong> rules matched by path prefix, evaluated longest-prefix-first.</li>
|
|
* </ol>
|
|
*
|
|
* <p>A request is subject to the global rule (if any) plus the single most specific path rule
|
|
* that matches. Instances are built through the nested {@link Builder}.</p>
|
|
*/
|
|
public final class RateLimitConfig {
|
|
|
|
/**
|
|
* Rule applied to every request, or {@code null} if no global rule is configured.
|
|
*/
|
|
private final Rule globalRule;
|
|
/**
|
|
* Rules matched by exact path equality, keyed by path.
|
|
*/
|
|
private final Map<String, Rule> exactPathRules;
|
|
/**
|
|
* Prefix rules, pre-sorted longest-prefix-first so the most specific match wins.
|
|
*/
|
|
private final List<PrefixRule> prefixRules;
|
|
/**
|
|
* Every distinct limiter referenced by any rule, by identity; used for periodic cleanup.
|
|
*/
|
|
private final Set<RateLimiter> allLimiters;
|
|
|
|
/**
|
|
* Builds an immutable configuration from a {@link Builder}, copying the exact-path rules
|
|
* and sorting the prefix rules by descending prefix length.
|
|
*
|
|
* @param b the builder carrying the configured rules
|
|
*/
|
|
private RateLimitConfig(Builder b) {
|
|
this.globalRule = b.globalRule;
|
|
this.exactPathRules = Map.copyOf(b.exactPathRules);
|
|
this.prefixRules = b.prefixRules.stream()
|
|
.sorted((a, c) -> Integer.compare(c.prefix.length(), a.prefix.length()))
|
|
.toList();
|
|
|
|
Set<RateLimiter> limiters = Collections.newSetFromMap(new IdentityHashMap<>());
|
|
if (globalRule != null) limiters.add(globalRule.limiter());
|
|
for (Rule r : exactPathRules.values()) limiters.add(r.limiter());
|
|
for (PrefixRule pr : prefixRules) limiters.add(pr.rule.limiter());
|
|
this.allLimiters = Collections.unmodifiableSet(limiters);
|
|
}
|
|
|
|
/**
|
|
* Creates a new, empty {@link Builder}.
|
|
*
|
|
* @return a fresh builder
|
|
*/
|
|
public static Builder builder() {
|
|
return new Builder();
|
|
}
|
|
|
|
/**
|
|
* Returns the ordered list of rules that apply to the given path.
|
|
*
|
|
* <p>The list contains the global rule first (if configured) followed by at most one
|
|
* path-specific rule: the exact-path rule if one matches, otherwise the longest matching
|
|
* prefix rule. The returned list may be empty if no rule applies.</p>
|
|
*
|
|
* @param path the request path
|
|
* @return the applicable rules, in evaluation order
|
|
*/
|
|
public List<Rule> rulesFor(String path) {
|
|
List<Rule> rules = new ArrayList<>(2);
|
|
if (globalRule != null) rules.add(globalRule);
|
|
|
|
Rule exact = exactPathRules.get(path);
|
|
if (exact != null) {
|
|
rules.add(exact);
|
|
return rules;
|
|
}
|
|
for (PrefixRule pr : prefixRules) {
|
|
if (path.startsWith(pr.prefix)) {
|
|
rules.add(pr.rule);
|
|
return rules;
|
|
}
|
|
}
|
|
return rules;
|
|
}
|
|
|
|
/**
|
|
* Returns every distinct limiter referenced by this configuration, for periodic state
|
|
* eviction by {@link RateLimitGate}.
|
|
*
|
|
* @return the immutable set of distinct limiters (de-duplicated by identity)
|
|
*/
|
|
public Set<RateLimiter> allLimiters() {
|
|
return allLimiters;
|
|
}
|
|
|
|
/**
|
|
* A single rate-limit rule: a limiter, the key resolver feeding it, and a name used to
|
|
* namespace keys and aid diagnostics.
|
|
*
|
|
* @param limiter the limiter that enforces the quota
|
|
* @param keyResolver resolves the per-request key the limiter buckets on
|
|
* @param name a human-readable label (e.g. {@code "global"} or a path/prefix)
|
|
*/
|
|
public record Rule(RateLimiter limiter, KeyResolver keyResolver, String name) {
|
|
}
|
|
|
|
/**
|
|
* Internal pairing of a path prefix with the rule that applies to paths starting with it.
|
|
*
|
|
* @param prefix the path prefix
|
|
* @param rule the rule to apply for matching paths
|
|
*/
|
|
private record PrefixRule(String prefix, Rule rule) {
|
|
}
|
|
|
|
/**
|
|
* Fluent builder for {@link RateLimitConfig}.
|
|
*/
|
|
public static final class Builder {
|
|
/**
|
|
* Accumulated exact-path rules, keyed by path.
|
|
*/
|
|
private final Map<String, Rule> exactPathRules = new HashMap<>();
|
|
/**
|
|
* Accumulated prefix rules.
|
|
*/
|
|
private final List<PrefixRule> prefixRules = new ArrayList<>();
|
|
/**
|
|
* The global rule, if configured.
|
|
*/
|
|
private Rule globalRule;
|
|
|
|
/**
|
|
* Creates a builder with no rules configured. Obtain instances via
|
|
* {@link RateLimitConfig#builder()}.
|
|
*/
|
|
public Builder() {
|
|
}
|
|
|
|
/**
|
|
* Sets the global rule applied to every request.
|
|
*
|
|
* @param limiter the limiter enforcing the global quota
|
|
* @param keys the key resolver for the global rule
|
|
* @return this builder, for fluent chaining
|
|
*/
|
|
public Builder global(RateLimiter limiter, KeyResolver keys) {
|
|
this.globalRule = new Rule(limiter, keys, "global");
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Adds a rule that applies only to requests whose path equals {@code path} exactly.
|
|
*
|
|
* @param path the exact request path
|
|
* @param limiter the limiter enforcing the quota
|
|
* @param keys the key resolver for this rule
|
|
* @return this builder, for fluent chaining
|
|
*/
|
|
public Builder forPath(String path, RateLimiter limiter, KeyResolver keys) {
|
|
exactPathRules.put(path, new Rule(limiter, keys, path));
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Adds a rule that applies to requests whose path starts with {@code prefix}. When
|
|
* several prefixes match, the longest one wins.
|
|
*
|
|
* @param prefix the path prefix
|
|
* @param limiter the limiter enforcing the quota
|
|
* @param keys the key resolver for this rule
|
|
* @return this builder, for fluent chaining
|
|
*/
|
|
public Builder forPrefix(String prefix, RateLimiter limiter, KeyResolver keys) {
|
|
prefixRules.add(new PrefixRule(prefix, new Rule(limiter, keys, prefix + "*")));
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Builds the immutable {@link RateLimitConfig}.
|
|
*
|
|
* @return the configured instance
|
|
*/
|
|
public RateLimitConfig build() {
|
|
return new RateLimitConfig(this);
|
|
}
|
|
}
|
|
}
|