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 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
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

58

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

63

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

65

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

67

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

70

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 edu/ucsb/cs156/courses/filters/RateLimitFilter::recordRateLimitedIP → KILLED

71

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

72

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

73

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

80

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

82

1.1
Location : recordRateLimitedIP
Killed by : edu.ucsb.cs156.courses.filters.RateLimitFilterTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.filters.RateLimitFilterTests]/[method:testRecordRateLimitedIP_existingRecord()]
Replaced long addition with subtraction → KILLED

2.2
Location : recordRateLimitedIP
Killed by : edu.ucsb.cs156.courses.filters.RateLimitFilterTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.filters.RateLimitFilterTests]/[method:testRecordRateLimitedIP_existingRecord()]
removed call to edu/ucsb/cs156/courses/entities/RateLimitedIP::setRequestCount → KILLED

83

1.1
Location : recordRateLimitedIP
Killed by : edu.ucsb.cs156.courses.filters.RateLimitFilterTests.[engine:junit-jupiter]/[class:edu.ucsb.cs156.courses.filters.RateLimitFilterTests]/[method:testRecordRateLimitedIP_existingRecord()]
removed call to edu/ucsb/cs156/courses/entities/RateLimitedIP::setLastRequestAt → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0