| 1 | package edu.ucsb.cs156.courses.filters; | |
| 2 | ||
| 3 | import com.github.benmanes.caffeine.cache.Cache; | |
| 4 | import com.github.benmanes.caffeine.cache.Caffeine; | |
| 5 | import io.github.bucket4j.Bandwidth; | |
| 6 | import io.github.bucket4j.Bucket; | |
| 7 | import io.github.bucket4j.Refill; | |
| 8 | import jakarta.servlet.FilterChain; | |
| 9 | import jakarta.servlet.ServletException; | |
| 10 | import jakarta.servlet.http.HttpServletRequest; | |
| 11 | import jakarta.servlet.http.HttpServletResponse; | |
| 12 | import java.io.IOException; | |
| 13 | import java.time.Duration; | |
| 14 | import java.util.concurrent.TimeUnit; | |
| 15 | import org.springframework.http.HttpStatus; | |
| 16 | import org.springframework.web.filter.OncePerRequestFilter; | |
| 17 | ||
| 18 | public class RateLimitFilter extends OncePerRequestFilter { | |
| 19 | ||
| 20 | private final int initialBucketSize; | |
| 21 | private final int refillPerMinute; | |
| 22 | ||
| 23 | public RateLimitFilter(int initialBucketSize, int refillPerMinute) { | |
| 24 | this.initialBucketSize = initialBucketSize; | |
| 25 | this.refillPerMinute = refillPerMinute; | |
| 26 | } | |
| 27 | ||
| 28 | // Caffeine cache: Keys are IP addresses, Values are Bucket objects. | |
| 29 | // Entries expire 1 hour after the last access. | |
| 30 | private final Cache<String, Bucket> cache = | |
| 31 | Caffeine.newBuilder() | |
| 32 | .expireAfterAccess(1, TimeUnit.HOURS) | |
| 33 | .maximumSize(10000) // Protects against memory exhaustion from botnets | |
| 34 | .build(); | |
| 35 | ||
| 36 | Bucket createNewBucket() { | |
| 37 | Bandwidth limit = | |
| 38 | Bandwidth.classic( | |
| 39 | initialBucketSize, Refill.intervally(refillPerMinute, Duration.ofMinutes(1))); | |
| 40 |
1
1. createNewBucket : replaced return value with null for edu/ucsb/cs156/courses/filters/RateLimitFilter::createNewBucket → KILLED |
return Bucket.builder().addLimit(limit).build(); |
| 41 | } | |
| 42 | ||
| 43 | @Override | |
| 44 | protected void doFilterInternal( | |
| 45 | HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | |
| 46 | throws ServletException, IOException { | |
| 47 | ||
| 48 | // Prefer X-Forwarded-For when behind a proxy like Nginx or AWS load balancers | |
| 49 | String xForwardedFor = request.getHeader("X-Forwarded-For"); | |
| 50 | String ip = | |
| 51 |
2
1. doFilterInternal : negated conditional → KILLED 2. doFilterInternal : negated conditional → KILLED |
(xForwardedFor != null && !xForwardedFor.isBlank()) |
| 52 | ? xForwardedFor.split(",")[0].trim() | |
| 53 | : request.getRemoteAddr(); | |
| 54 | ||
| 55 | // Get or create the bucket for this IP | |
| 56 |
1
1. lambda$doFilterInternal$0 : replaced return value with null for edu/ucsb/cs156/courses/filters/RateLimitFilter::lambda$doFilterInternal$0 → KILLED |
Bucket bucket = cache.get(ip, key -> createNewBucket()); |
| 57 | ||
| 58 |
1
1. doFilterInternal : negated conditional → KILLED |
if (bucket.tryConsume(1)) { |
| 59 | // Success: Continue to the next filter/controller | |
| 60 |
1
1. doFilterInternal : removed call to jakarta/servlet/FilterChain::doFilter → KILLED |
filterChain.doFilter(request, response); |
| 61 | } else { | |
| 62 | // Failure: Too many requests | |
| 63 |
1
1. doFilterInternal : removed call to jakarta/servlet/http/HttpServletResponse::setStatus → KILLED |
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); |
| 64 |
1
1. doFilterInternal : removed call to jakarta/servlet/http/HttpServletResponse::setContentType → KILLED |
response.setContentType("text/plain"); |
| 65 |
1
1. doFilterInternal : removed call to java/io/PrintWriter::write → KILLED |
response.getWriter().write("Too many requests. Your IP has been throttled."); |
| 66 | } | |
| 67 | } | |
| 68 | } | |
Mutations | ||
| 40 |
1.1 |
|
| 51 |
1.1 2.2 |
|
| 56 |
1.1 |
|
| 58 |
1.1 |
|
| 60 |
1.1 |
|
| 63 |
1.1 |
|
| 64 |
1.1 |
|
| 65 |
1.1 |