1package dev.rafex.ether.http.jetty12;
29import java.time.Clock;
30import java.util.concurrent.ConcurrentHashMap;
32import org.eclipse.jetty.server.Handler;
33import org.eclipse.jetty.server.Request;
34import org.eclipse.jetty.server.Response;
35import org.eclipse.jetty.util.Callback;
37import dev.rafex.ether.http.jetty12.response.JettyApiErrorResponses;
38import dev.rafex.ether.http.security.proxy.TrustedProxyPolicy;
39import dev.rafex.ether.http.security.ratelimit.RateLimitPolicy;
41final class JettyRateLimitHandler
extends Handler.Wrapper {
45 private final JettyApiErrorResponses errorResponses;
46 private final Clock clock;
47 private final ConcurrentHashMap<String, WindowCounter> counters =
new ConcurrentHashMap<>();
49 JettyRateLimitHandler(
final Handler next,
final RateLimitPolicy policy,
final TrustedProxyPolicy trustedProxyPolicy,
50 final JettyApiErrorResponses errorResponses) {
51 this(next, policy, trustedProxyPolicy, errorResponses, Clock.systemUTC());
54 JettyRateLimitHandler(
final Handler next,
final RateLimitPolicy policy,
final TrustedProxyPolicy trustedProxyPolicy,
55 final JettyApiErrorResponses errorResponses,
final Clock clock) {
58 this.trustedProxyPolicy = trustedProxyPolicy;
59 this.errorResponses = errorResponses;
64 public boolean handle(
final Request request,
final Response response,
final Callback callback)
throws Exception {
65 final var decision = register(request);
66 response.getHeaders().put(
"RateLimit-Limit", Integer.toString(decision.limit()));
67 response.getHeaders().put(
"RateLimit-Remaining", Integer.toString(decision.remaining()));
68 response.getHeaders().put(
"RateLimit-Reset", Long.toString(decision.resetAtEpochSecond()));
70 if (!decision.allowed()) {
71 errorResponses.error(response, callback, 429,
"too_many_requests",
"rate_limit_exceeded",
72 "request rate limit exceeded", request.getHttpURI().getPath());
76 return super.handle(request, response, callback);
79 private Decision register(
final Request request) {
80 final long nowEpochSecond = clock.instant().getEpochSecond();
81 final long windowSeconds = Math.max(1, policy.windowSeconds());
82 final long windowStart = (nowEpochSecond / windowSeconds) * windowSeconds;
83 final long resetAt = windowStart + windowSeconds;
84 final int limit = Math.max(0, policy.maxRequests()) + Math.max(0, policy.burst());
85 final String key = keyFor(request);
87 final var counter = counters.compute(key, (ignored, existing) -> {
88 if (existing ==
null || existing.windowStartEpochSecond != windowStart) {
89 return new WindowCounter(windowStart, 1);
95 final boolean allowed = counter.count <= limit;
96 final int remaining = Math.max(0, limit - counter.count);
97 return new Decision(allowed, limit, remaining, resetAt);
100 private String keyFor(
final Request request) {
104 final var clientIp = JettyRequestIpResolver.resolve(request, trustedProxyPolicy);
105 return clientIp ==
null || clientIp.isBlank() ?
"unknown" : clientIp;
108 private record Decision(
boolean allowed,
int limit,
int remaining,
long resetAtEpochSecond) {
111 private static final class WindowCounter {
112 private final long windowStartEpochSecond;
115 private WindowCounter(
final long windowStartEpochSecond,
final int count) {
116 this.windowStartEpochSecond = windowStartEpochSecond;
record TrustedProxyPolicy(List< String > trustedSources, boolean trustForwardedHeader, boolean forwardedOnly, boolean preferRightMostForwardedFor)
Política para configurar proxy de confianza en servidores.
record RateLimitPolicy(Scope scope, int maxRequests, int windowSeconds, int burst, int maxConcurrentRequests)
Política de control de tasa (rate limiting) para prevenir abusos.