55import java .time .Duration ;
66import java .util .Base64 ;
77import java .util .Objects ;
8+ import java .util .Properties ;
89import java .util .concurrent .ConcurrentHashMap ;
910
1011import org .apache .logging .log4j .LogManager ;
1112import org .apache .logging .log4j .Logger ;
13+ import org .springframework .beans .factory .annotation .Autowired ;
1214import org .springframework .stereotype .Component ;
1315
16+ import de .ids_mannheim .korap .config .KustvaktConfiguration ;
17+ import de .ids_mannheim .korap .utils .TimeUtils ;
18+ import jakarta .annotation .PostConstruct ;
1419import jakarta .annotation .Priority ;
1520import jakarta .ws .rs .Priorities ;
1621import jakarta .ws .rs .WebApplicationException ;
1722import jakarta .ws .rs .container .ContainerRequestContext ;
1823import jakarta .ws .rs .container .ContainerRequestFilter ;
1924import jakarta .ws .rs .core .HttpHeaders ;
2025import jakarta .ws .rs .core .Response ;
26+ import lombok .Getter ;
2127
22- /** Implemented with AI assistance
23- *
28+ /**
2429 * Simple in-memory rate limitation for authenticated users.
2530 * <p>
2631 * Keyed by bearer token (preferred) or username (fallback).
2732 * <p>
2833 * Note: In-memory means per-JVM only. For clustered deployments, use Redis/etc.
34+ *
35+ * Implemented with AI assistance
2936 */
3037@ Component
3138@ Priority (Priorities .AUTHORIZATION )
@@ -34,53 +41,96 @@ public class RateLimitFilter implements ContainerRequestFilter {
3441 private static final Logger jlog = LogManager
3542 .getLogger (RateLimitFilter .class );
3643
37- // Defaults: 60 requests per minute per key
38- // Keep these conservative and easy to change later via config injection.
39- private static final long REFILL_TOKENS = 60 ;
40- private static final Duration REFILL_PERIOD = Duration .ofMinutes (1 );
41- public static final long BURST_CAPACITY = 60 ;
42-
43- /**
44- * Prevent unbounded growth: keep at most this many distinct keys in-memory.
45- */
46- private static final int MAX_BUCKETS = 10_000 ;
44+ @ Autowired
45+ private KustvaktConfiguration config ;
4746
48- /**
49- * Evict buckets that haven't been seen for this long.
50- */
51- private static final Duration BUCKET_TTL = Duration .ofHours (6 );
47+ // Rate limiting configuration (loaded from kustvakt.conf)
48+ private long refillTokens ;
49+ private Duration refillPeriod ;
50+ @ Getter
51+ private long burstCapacity ;
52+ private int maxBuckets ;
53+ private Duration bucketTTL ;
5254
5355 private final ConcurrentHashMap <String , BucketEntry > buckets = new ConcurrentHashMap <>();
5456
57+ @ PostConstruct
58+ private void initializeConfiguration () {
59+ try {
60+ Properties props = config .getProperties ();
61+
62+ // Handle case where properties might not be initialized yet
63+ if (props == null ) {
64+ jlog .warn ("KustvaktConfiguration properties not available, using default rate limiting values" );
65+ setDefaultValues ();
66+ return ;
67+ }
68+
69+ // Load rate limiting settings from kustvakt.conf with sensible defaults
70+ String refillTokensStr = props .getProperty ("ratelimit.refill.tokens" , "60" );
71+ this .refillTokens = Long .parseLong (refillTokensStr );
72+
73+ String refillPeriodStr = props .getProperty ("ratelimit.refill.period" , "1M" );
74+ this .refillPeriod = Duration .ofSeconds (TimeUtils .convertTimeToSeconds (refillPeriodStr ));
75+
76+ String burstCapacityStr = props .getProperty ("ratelimit.burst.capacity" , "60" );
77+ this .burstCapacity = Long .parseLong (burstCapacityStr );
78+
79+ String maxBucketsStr = props .getProperty ("ratelimit.max.buckets" , "10000" );
80+ this .maxBuckets = Integer .parseInt (maxBucketsStr );
81+
82+ String bucketTTLStr = props .getProperty ("ratelimit.bucket.ttl" , "6H" );
83+ this .bucketTTL = Duration .ofSeconds (TimeUtils .convertTimeToSeconds (bucketTTLStr ));
84+
85+ jlog .info ("Rate limiting initialized: refillTokens={}, refillPeriod={}, burstCapacity={}, maxBuckets={}, bucketTTL={}" ,
86+ refillTokens , refillPeriod , burstCapacity , maxBuckets , bucketTTL );
87+ } catch (Exception e ) {
88+ jlog .error ("Failed to initialize rate limiting configuration, using defaults" , e );
89+ setDefaultValues ();
90+ }
91+ }
92+
93+ private void setDefaultValues () {
94+ this .refillTokens = 60 ;
95+ this .refillPeriod = Duration .ofMinutes (1 );
96+ this .burstCapacity = 60 ;
97+ this .maxBuckets = 10000 ;
98+ this .bucketTTL = Duration .ofHours (6 );
99+ jlog .info ("Rate limiting initialized with defaults: refillTokens={}, refillPeriod={}, burstCapacity={}, maxBuckets={}, bucketTTL={}" ,
100+ refillTokens , refillPeriod , burstCapacity , maxBuckets , bucketTTL );
101+ }
102+
55103 @ Override
56104 public void filter (ContainerRequestContext request ) {
57105 // Only apply to authenticated requests
58106 if (request .getSecurityContext () == null
59107 || request .getSecurityContext ().getUserPrincipal () == null ) {
108+ jlog .debug ("Skipping rate limiting - no SecurityContext or UserPrincipal" );
60109 return ;
61110 }
62111
63112 String key = resolveKey (request );
64113 if (key == null ) {
114+ jlog .debug ("Skipping rate limiting - could not resolve key" );
65115 return ;
66116 }
67117
118+ jlog .debug ("Applying rate limiting for key: {}" , key );
119+
68120 long now = System .currentTimeMillis ();
69121
70122 // Opportunistic cleanup to avoid memory growth.
71- // Do it only on inserts or if we grow too large.
72- if (buckets .size () > MAX_BUCKETS ) {
123+ if (buckets .size () > maxBuckets ) {
73124 cleanupOldEntries (now );
74125 }
75126
76127 BucketEntry entry = buckets .compute (key , (k , existing ) -> {
77128 if (existing == null ) {
78- // If we're still too large, try another cleanup pass before adding.
79- if (buckets .size () > MAX_BUCKETS ) {
129+ if (buckets .size () > maxBuckets ) {
80130 cleanupOldEntries (now );
81131 }
82- return new BucketEntry (new TokenBucket (BURST_CAPACITY ,
83- REFILL_TOKENS , REFILL_PERIOD .toMillis ()), now );
132+ return new BucketEntry (new TokenBucket (burstCapacity ,
133+ refillTokens , refillPeriod .toMillis ()), now );
84134 }
85135 existing .lastSeenAtMillis = now ;
86136 return existing ;
@@ -90,20 +140,29 @@ public void filter (ContainerRequestContext request) {
90140 long retryAfterSeconds = Math
91141 .max (1 , entry .bucket .millisUntilNextToken () / 1000 );
92142
143+ jlog .info ("Rate limit exceeded for key: {}, retry after {} seconds" , key , retryAfterSeconds );
93144 throw new WebApplicationException (Response .status (429 )
94145 .header ("Retry-After" , String .valueOf (retryAfterSeconds ))
95146 .entity ("Rate limit exceeded" )
96147 .build ());
97148 }
98149 }
99150
151+ /**
152+ * Clear all rate limit buckets. For testing purposes only.
153+ */
154+ public void clearBuckets () {
155+ buckets .clear ();
156+ jlog .info ("Rate limit buckets cleared" );
157+ }
158+
100159 private void cleanupOldEntries (long nowMillis ) {
101- final long cutoff = nowMillis - BUCKET_TTL .toMillis ();
160+ final long cutoff = nowMillis - bucketTTL .toMillis ();
102161 buckets .entrySet ().removeIf (e -> e .getValue ().lastSeenAtMillis < cutoff );
103162
104163 // Still too big? Remove arbitrary entries (best-effort bound).
105- if (buckets .size () > MAX_BUCKETS ) {
106- int toRemove = buckets .size () - MAX_BUCKETS ;
164+ if (buckets .size () > maxBuckets ) {
165+ int toRemove = buckets .size () - maxBuckets ;
107166 for (String k : buckets .keySet ()) {
108167 buckets .remove (k );
109168 if (--toRemove <= 0 )
@@ -123,6 +182,8 @@ private String resolveKey (ContainerRequestContext request) {
123182 }
124183 }
125184
185+ // Skip unauthenticated requests. DemoUserFilter sets username guest
186+ // for such requests.
126187 // Fallback to username/principal name
127188// String name = request.getSecurityContext().getUserPrincipal().getName();
128189// if (name != null && !name.isBlank()) {
@@ -208,4 +269,4 @@ private static final class BucketEntry {
208269 this .lastSeenAtMillis = lastSeenAtMillis ;
209270 }
210271 }
211- }
272+ }
0 commit comments