forked from apache/fineract
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
173 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
...src/main/java/org/apache/fineract/infrastructure/security/ratelimit/EnforceRateLimit.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package org.apache.fineract.infrastructure.security.ratelimit; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
@Target({ ElementType.METHOD, ElementType.TYPE }) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
public @interface EnforceRateLimit {} |
37 changes: 37 additions & 0 deletions
37
.../java/org/apache/fineract/infrastructure/security/ratelimit/InMemoryRateLimitService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package org.apache.fineract.infrastructure.security.ratelimit; | ||
|
||
import com.github.benmanes.caffeine.cache.Cache; | ||
import com.github.benmanes.caffeine.cache.Caffeine; | ||
import jakarta.annotation.PostConstruct; | ||
import java.util.Optional; | ||
import java.util.concurrent.TimeUnit; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class InMemoryRateLimitService implements RateLimitService { | ||
|
||
@Value("${fineract.security.ratelimit.max-attempts-per-host-per-minute}") | ||
private int maxAttemptsPerHostPerMinute; | ||
|
||
private Cache<String, Integer> cache; | ||
|
||
@PostConstruct | ||
public void setup() { | ||
cache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).maximumSize(100_000).build(); | ||
} | ||
|
||
@Override | ||
public boolean isRateLimited(String remoteAddress) { | ||
Integer existingAttempts = cache.getIfPresent(remoteAddress); | ||
int currentAttempt = 1 + Optional.ofNullable(existingAttempts).orElse(0); | ||
cache.put(remoteAddress, currentAttempt); | ||
|
||
return currentAttempt > maxAttemptsPerHostPerMinute; | ||
} | ||
|
||
@Override | ||
public void resetRateLimit(String remoteAddress) { | ||
cache.invalidate(remoteAddress); | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
.../src/main/java/org/apache/fineract/infrastructure/security/ratelimit/RateLimitAspect.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package org.apache.fineract.infrastructure.security.ratelimit; | ||
|
||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.ws.rs.core.Response; | ||
import lombok.RequiredArgsConstructor; | ||
import org.aspectj.lang.ProceedingJoinPoint; | ||
import org.aspectj.lang.annotation.Around; | ||
import org.aspectj.lang.annotation.Aspect; | ||
import org.aspectj.lang.annotation.Pointcut; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.context.request.RequestContextHolder; | ||
import org.springframework.web.context.request.ServletRequestAttributes; | ||
|
||
@Aspect | ||
@Component | ||
@RequiredArgsConstructor | ||
public class RateLimitAspect { | ||
|
||
private final RateLimitService rateLimitService; | ||
|
||
@Pointcut("@annotation(EnforceRateLimit) || @within(EnforceRateLimit)") | ||
public void pointcut() {} | ||
|
||
@Around("pointcut()") | ||
public Object aroundRestCall(ProceedingJoinPoint joinPoint) throws Throwable { | ||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); | ||
String remoteAddress = request.getRemoteAddr(); | ||
|
||
if (rateLimitService.isRateLimited(remoteAddress)) { | ||
return Response.status(429).entity("Too many requests - try again later.").build(); | ||
|
||
} else { | ||
return joinPoint.proceed(); | ||
} | ||
} | ||
} |
9 changes: 9 additions & 0 deletions
9
...src/main/java/org/apache/fineract/infrastructure/security/ratelimit/RateLimitService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package org.apache.fineract.infrastructure.security.ratelimit; | ||
|
||
public interface RateLimitService { | ||
|
||
boolean isRateLimited(String remoteAddress); | ||
|
||
void resetRateLimit(String remoteAddress); | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
52 changes: 52 additions & 0 deletions
52
...a/org/apache/fineract/infrastructure/security/ratelimit/TestInMemoryRateLimitService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package org.apache.fineract.infrastructure.security.ratelimit; | ||
|
||
import com.github.benmanes.caffeine.cache.Caffeine; | ||
import org.junit.jupiter.api.Test; | ||
import org.springframework.test.util.ReflectionTestUtils; | ||
|
||
import java.util.concurrent.TimeUnit; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertFalse; | ||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
||
class TestInMemoryRateLimitService { | ||
|
||
@Test | ||
public void testDefaultRateLimiter() { | ||
InMemoryRateLimitService ratelimiter = new InMemoryRateLimitService(); | ||
ratelimiter.setup(); | ||
ReflectionTestUtils.setField(ratelimiter, "maxAttemptsPerHostPerMinute", 3); | ||
|
||
assertFalse(ratelimiter.isRateLimited("127.0.0.1")); | ||
assertFalse(ratelimiter.isRateLimited("127.0.0.1")); | ||
assertFalse(ratelimiter.isRateLimited("127.0.0.1")); | ||
assertTrue(ratelimiter.isRateLimited("127.0.0.1")); | ||
} | ||
|
||
@Test | ||
public void testTimeEviction() throws InterruptedException { | ||
InMemoryRateLimitService ratelimiter = new InMemoryRateLimitService(); | ||
ratelimiter.setup(); | ||
ReflectionTestUtils.setField(ratelimiter, "maxAttemptsPerHostPerMinute", 2); | ||
ReflectionTestUtils.setField(ratelimiter, "cache", Caffeine.newBuilder().expireAfterWrite(500, TimeUnit.MILLISECONDS).maximumSize(100_000).build()); | ||
|
||
assertFalse(ratelimiter.isRateLimited("127.0.0.1")); | ||
assertFalse(ratelimiter.isRateLimited("127.0.0.1")); | ||
assertTrue(ratelimiter.isRateLimited("127.0.0.1")); | ||
Thread.sleep(1_000L); | ||
assertFalse(ratelimiter.isRateLimited("127.0.0.1")); | ||
} | ||
|
||
@Test | ||
public void testReset() { | ||
InMemoryRateLimitService ratelimiter = new InMemoryRateLimitService(); | ||
ratelimiter.setup(); | ||
ReflectionTestUtils.setField(ratelimiter, "maxAttemptsPerHostPerMinute", 2); | ||
assertFalse(ratelimiter.isRateLimited("127.0.0.1")); | ||
assertFalse(ratelimiter.isRateLimited("127.0.0.1")); | ||
assertTrue(ratelimiter.isRateLimited("127.0.0.1")); | ||
|
||
ratelimiter.resetRateLimit("127.0.0.1"); | ||
assertFalse(ratelimiter.isRateLimited("127.0.0.1")); | ||
} | ||
} |