| 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 edu.ucsb.cs156.courses.entities.RateLimitedIP; | |
| 6 | import edu.ucsb.cs156.courses.repositories.RateLimitedIPRepository; | |
| 7 | import io.github.bucket4j.Bandwidth; | |
| 8 | import io.github.bucket4j.Bucket; | |
| 9 | import io.github.bucket4j.Refill; | |
| 10 | import jakarta.servlet.FilterChain; | |
| 11 | import jakarta.servlet.ServletException; | |
| 12 | import jakarta.servlet.http.HttpServletRequest; | |
| 13 | import jakarta.servlet.http.HttpServletResponse; | |
| 14 | import java.io.IOException; | |
| 15 | import java.time.Duration; | |
| 16 | import java.time.ZonedDateTime; | |
| 17 | import java.util.Optional; | |
| 18 | import java.util.concurrent.TimeUnit; | |
| 19 | import org.springframework.http.HttpStatus; | |
| 20 | import org.springframework.web.filter.OncePerRequestFilter; | |
| 21 | ||
| 22 | public class RateLimitFilter extends OncePerRequestFilter { | |
| 23 | ||
| 24 | private final int initialBucketSize; | |
| 25 | private final int refillPerMinute; | |
| 26 | private final RateLimitedIPRepository rateLimitedIPRepository; | |
| 27 | ||
| 28 | public RateLimitFilter( | |
| 29 | int initialBucketSize, int refillPerMinute, RateLimitedIPRepository rateLimitedIPRepository) { | |
| 30 | this.initialBucketSize = initialBucketSize; | |
| 31 | this.refillPerMinute = refillPerMinute; | |
| 32 | this.rateLimitedIPRepository = rateLimitedIPRepository; | |
| 33 | } | |
| 34 | ||
| 35 | // Caffeine cache: Keys are IP addresses, Values are Bucket objects. | |
| 36 | // Entries expire 1 hour after the last access. | |
| 37 | private final Cache<String, Bucket> cache = | |
| 38 | Caffeine.newBuilder() | |
| 39 | .expireAfterAccess(1, TimeUnit.HOURS) | |
| 40 | .maximumSize(10000) // Protects against memory exhaustion from botnets | |
| 41 | .build(); | |
| 42 | ||
| 43 | Bucket createNewBucket() { | |
| 44 | Bandwidth limit = | |
| 45 | Bandwidth.classic( | |
| 46 | initialBucketSize, Refill.intervally(refillPerMinute, Duration.ofMinutes(1))); | |
| 47 |
1
1. createNewBucket : replaced return value with null for edu/ucsb/cs156/courses/filters/RateLimitFilter::createNewBucket → KILLED |
return Bucket.builder().addLimit(limit).build(); |
| 48 | } | |
| 49 | ||
| 50 | @Override | |
| 51 | protected void doFilterInternal( | |
| 52 | HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | |
| 53 | throws ServletException, IOException { | |
| 54 | ||
| 55 | // Prefer X-Forwarded-For when behind a proxy like Nginx or AWS load balancers | |
| 56 | String xForwardedFor = request.getHeader("X-Forwarded-For"); | |
| 57 | String ip = | |
| 58 |
2
1. doFilterInternal : negated conditional → KILLED 2. doFilterInternal : negated conditional → KILLED |
(xForwardedFor != null && !xForwardedFor.isBlank()) |
| 59 | ? xForwardedFor.split(",")[0].trim() | |
| 60 | : request.getRemoteAddr(); | |
| 61 | ||
| 62 | // Get or create the bucket for this IP | |
| 63 |
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()); |
| 64 | ||
| 65 |
1
1. doFilterInternal : negated conditional → KILLED |
if (bucket.tryConsume(1)) { |
| 66 | // Success: Continue to the next filter/controller | |
| 67 |
1
1. doFilterInternal : removed call to jakarta/servlet/FilterChain::doFilter → KILLED |
filterChain.doFilter(request, response); |
| 68 | } else { | |
| 69 | // Failure: Too many requests — record this in the database | |
| 70 |
1
1. doFilterInternal : removed call to edu/ucsb/cs156/courses/filters/RateLimitFilter::recordRateLimitedIP → KILLED |
recordRateLimitedIP(ip); |
| 71 |
1
1. doFilterInternal : removed call to jakarta/servlet/http/HttpServletResponse::setStatus → KILLED |
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); |
| 72 |
1
1. doFilterInternal : removed call to jakarta/servlet/http/HttpServletResponse::setContentType → KILLED |
response.setContentType("text/plain"); |
| 73 |
1
1. doFilterInternal : removed call to java/io/PrintWriter::write → KILLED |
response.getWriter().write("Too many requests. Your IP has been throttled."); |
| 74 | } | |
| 75 | } | |
| 76 | ||
| 77 | void recordRateLimitedIP(String ip) { | |
| 78 | Optional<RateLimitedIP> existing = rateLimitedIPRepository.findById(ip); | |
| 79 | RateLimitedIP record; | |
| 80 |
1
1. recordRateLimitedIP : negated conditional → KILLED |
if (existing.isPresent()) { |
| 81 | record = existing.get(); | |
| 82 |
2
1. recordRateLimitedIP : Replaced long addition with subtraction → KILLED 2. recordRateLimitedIP : removed call to edu/ucsb/cs156/courses/entities/RateLimitedIP::setRequestCount → KILLED |
record.setRequestCount(record.getRequestCount() + 1); |
| 83 |
1
1. recordRateLimitedIP : removed call to edu/ucsb/cs156/courses/entities/RateLimitedIP::setLastRequestAt → KILLED |
record.setLastRequestAt(ZonedDateTime.now()); |
| 84 | } else { | |
| 85 | record = | |
| 86 | RateLimitedIP.builder() | |
| 87 | .ipAddress(ip) | |
| 88 | .requestCount(1) | |
| 89 | .lastRequestAt(ZonedDateTime.now()) | |
| 90 | .build(); | |
| 91 | } | |
| 92 | rateLimitedIPRepository.save(record); | |
| 93 | } | |
| 94 | } | |
Mutations | ||
| 47 |
1.1 |
|
| 58 |
1.1 2.2 |
|
| 63 |
1.1 |
|
| 65 |
1.1 |
|
| 67 |
1.1 |
|
| 70 |
1.1 |
|
| 71 |
1.1 |
|
| 72 |
1.1 |
|
| 73 |
1.1 |
|
| 80 |
1.1 |
|
| 82 |
1.1 2.2 |
|
| 83 |
1.1 |