RateLimitFilter.java

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
Location : createNewBucket
Killed by : edu.ucsb.cs156.courses.filters.RateLimitFilterTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.filters.RateLimitFilterTests]/[method:testCreateNewBucketReturnsNonNullBucket()]
replaced return value with null for edu/ucsb/cs156/courses/filters/RateLimitFilter::createNewBucket → KILLED

51

1.1
Location : doFilterInternal
Killed by : edu.ucsb.cs156.courses.filters.RateLimitFilterTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.filters.RateLimitFilterTests]/[method:testXForwardedForHeaderUsedWhenPresent()]
negated conditional → KILLED

2.2
Location : doFilterInternal
Killed by : edu.ucsb.cs156.courses.filters.RateLimitFilterTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.filters.RateLimitFilterTests]/[method:testRequestAllowedWhenUnderRateLimit()]
negated conditional → KILLED

56

1.1
Location : lambda$doFilterInternal$0
Killed by : edu.ucsb.cs156.courses.filters.RateLimitFilterTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.filters.RateLimitFilterTests]/[method:testRequestAllowedWhenUnderRateLimit()]
replaced return value with null for edu/ucsb/cs156/courses/filters/RateLimitFilter::lambda$doFilterInternal$0 → KILLED

58

1.1
Location : doFilterInternal
Killed by : edu.ucsb.cs156.courses.filters.RateLimitFilterTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.filters.RateLimitFilterTests]/[method:testRequestAllowedWhenUnderRateLimit()]
negated conditional → KILLED

60

1.1
Location : doFilterInternal
Killed by : edu.ucsb.cs156.courses.filters.RateLimitFilterTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.filters.RateLimitFilterTests]/[method:testRequestAllowedWhenUnderRateLimit()]
removed call to jakarta/servlet/FilterChain::doFilter → KILLED

63

1.1
Location : doFilterInternal
Killed by : edu.ucsb.cs156.courses.filters.RateLimitFilterTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.filters.RateLimitFilterTests]/[method:testDifferentIpsHaveIndependentBuckets()]
removed call to jakarta/servlet/http/HttpServletResponse::setStatus → KILLED

64

1.1
Location : doFilterInternal
Killed by : edu.ucsb.cs156.courses.filters.RateLimitFilterTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.filters.RateLimitFilterTests]/[method:testRequestBlockedWhenOverRateLimit()]
removed call to jakarta/servlet/http/HttpServletResponse::setContentType → KILLED

65

1.1
Location : doFilterInternal
Killed by : edu.ucsb.cs156.courses.filters.RateLimitFilterTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.filters.RateLimitFilterTests]/[method:testRequestBlockedWhenOverRateLimit()]
removed call to java/io/PrintWriter::write → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0