diff --git a/src/main/java/com/changolaxtra/cloud/ratelimiter/RateLimiterApplication.java b/src/main/java/com/changolaxtra/cloud/ratelimiter/RateLimiterApplication.java deleted file mode 100644 index 36c6089..0000000 --- a/src/main/java/com/changolaxtra/cloud/ratelimiter/RateLimiterApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.changolaxtra.cloud.ratelimiter; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class RateLimiterApplication { - - public static void main(String[] args) { - SpringApplication.run(RateLimiterApplication.class, args); - } - -} diff --git a/src/main/java/com/changolaxtra/cloud/ratelimiter/annotations/ApiRateLimited.java b/src/main/java/com/changolaxtra/cloud/ratelimiter/annotations/ApiRateLimited.java new file mode 100644 index 0000000..999c25a --- /dev/null +++ b/src/main/java/com/changolaxtra/cloud/ratelimiter/annotations/ApiRateLimited.java @@ -0,0 +1,11 @@ +package com.changolaxtra.cloud.ratelimiter.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface ApiRateLimited { +} diff --git a/src/main/java/com/changolaxtra/cloud/ratelimiter/annotations/EnableApiRateLimiter.java b/src/main/java/com/changolaxtra/cloud/ratelimiter/annotations/EnableApiRateLimiter.java new file mode 100644 index 0000000..bc22b59 --- /dev/null +++ b/src/main/java/com/changolaxtra/cloud/ratelimiter/annotations/EnableApiRateLimiter.java @@ -0,0 +1,14 @@ +package com.changolaxtra.cloud.ratelimiter.annotations; + +import org.springframework.context.annotation.ComponentScan; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@ComponentScan(basePackages = "com.changolaxtra.cloud.ratelimiter") +public @interface EnableApiRateLimiter { +} diff --git a/src/main/java/com/changolaxtra/cloud/ratelimiter/aspect/ApiRateLimitedAspect.java b/src/main/java/com/changolaxtra/cloud/ratelimiter/aspect/ApiRateLimitedAspect.java new file mode 100644 index 0000000..0f9c158 --- /dev/null +++ b/src/main/java/com/changolaxtra/cloud/ratelimiter/aspect/ApiRateLimitedAspect.java @@ -0,0 +1,52 @@ +package com.changolaxtra.cloud.ratelimiter.aspect; + +import com.changolaxtra.cloud.ratelimiter.configuration.RateLimiterConfiguration; +import com.changolaxtra.cloud.ratelimiter.core.PlanLimitBucket; +import com.changolaxtra.cloud.ratelimiter.storage.PlanLimitStorage; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.util.Optional; + +@Slf4j +@Aspect +@Component +public class ApiRateLimitedAspect { + + private final PlanLimitStorage planLimitStorage; + private final RateLimiterConfiguration rateLimiterConfiguration; + + public ApiRateLimitedAspect(final PlanLimitStorage planLimitStorage, + final RateLimiterConfiguration rateLimiterConfiguration) { + this.planLimitStorage = planLimitStorage; + this.rateLimiterConfiguration = rateLimiterConfiguration; + } + + @Around("@annotation(com.changolaxtra.cloud.ratelimiter.annotations.ApiRateLimited)") + public Object rateLimitCheck(final ProceedingJoinPoint joinPoint) throws Throwable { + final String apiKey = getApiKey(); + final PlanLimitBucket planLimit = planLimitStorage.getPlanLimit(apiKey); + if (planLimit.isAllowed()) { + return joinPoint.proceed(); + } else { + return ResponseEntity + .status(HttpStatus.TOO_MANY_REQUESTS) + .body("API-Keys does not allow more calls at this moment."); + } + } + + private String getApiKey() { + return Optional.ofNullable(RequestContextHolder.getRequestAttributes()) + .map(requestAttributes -> (ServletRequestAttributes) requestAttributes) + .map(ServletRequestAttributes::getRequest) + .map(request -> request.getHeader(rateLimiterConfiguration.getApiKeyHeaderName())) + .orElse(rateLimiterConfiguration.getDefaultApiKey()); + } +} diff --git a/src/main/java/com/changolaxtra/cloud/ratelimiter/configuration/DefaultPlanLimitInitializer.java b/src/main/java/com/changolaxtra/cloud/ratelimiter/configuration/DefaultPlanLimitInitializer.java new file mode 100644 index 0000000..07530ce --- /dev/null +++ b/src/main/java/com/changolaxtra/cloud/ratelimiter/configuration/DefaultPlanLimitInitializer.java @@ -0,0 +1,45 @@ +package com.changolaxtra.cloud.ratelimiter.configuration; + +import com.changolaxtra.cloud.ratelimiter.core.PlanLimitBucket; +import com.changolaxtra.cloud.ratelimiter.exception.RateLimitException; +import com.changolaxtra.cloud.ratelimiter.core.policy.RateLimitPolicy; +import com.changolaxtra.cloud.ratelimiter.storage.PlanLimitStorage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class DefaultPlanLimitInitializer implements InitializingBean { + + private final PlanLimitStorage planLimitStorage; + private final RateLimiterConfiguration rateLimiterConfiguration; + + public DefaultPlanLimitInitializer(final PlanLimitStorage planLimitStorage, + final RateLimiterConfiguration rateLimiterConfiguration) { + this.planLimitStorage = planLimitStorage; + this.rateLimiterConfiguration = rateLimiterConfiguration; + } + + + @Override + public void afterPropertiesSet() throws Exception { + final RateLimitPolicy defaultPolicy = getDefaultPolicy(); + final boolean isDefaultPolicyCreated = planLimitStorage.saveOrUpdate(new PlanLimitBucket(defaultPolicy)); + if (!isDefaultPolicyCreated) { + log.error("Default policy wasn't created."); + throw new RateLimitException("Default policy wasn't created."); + } + log.info("Default policy was created correctly: {}", defaultPolicy); + } + + private RateLimitPolicy getDefaultPolicy() { + return RateLimitPolicy + .builder() + .apiKey(rateLimiterConfiguration.getDefaultApiKey()) + .allowedRequests(rateLimiterConfiguration.getDefaultAllowedRequests()) + .windowSizeInMilliSeconds(rateLimiterConfiguration.getDefaultWindowSizeInMilliSeconds()) + .isUnlimited(rateLimiterConfiguration.isDefaultIsUnlimited()) + .build(); + } +} diff --git a/src/main/java/com/changolaxtra/cloud/ratelimiter/configuration/RateLimiterConfiguration.java b/src/main/java/com/changolaxtra/cloud/ratelimiter/configuration/RateLimiterConfiguration.java index 1d32ad8..3938d7d 100644 --- a/src/main/java/com/changolaxtra/cloud/ratelimiter/configuration/RateLimiterConfiguration.java +++ b/src/main/java/com/changolaxtra/cloud/ratelimiter/configuration/RateLimiterConfiguration.java @@ -1,7 +1,26 @@ package com.changolaxtra.cloud.ratelimiter.configuration; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; +@Getter @Configuration public class RateLimiterConfiguration { + + @Value("${rate-limiter.apiKey-header-name}") + private String apiKeyHeaderName; + + @Value("${rate-limiter.default-policy.apiKey}") + private String defaultApiKey; + + @Value("${rate-limiter.default-policy.allowedRequests}") + private long defaultAllowedRequests; + + @Value("${rate-limiter.default-policy.windowSizeInMilliSeconds}") + private long defaultWindowSizeInMilliSeconds; + + @Value("${rate-limiter.default-policy.isUnlimited}") + private boolean defaultIsUnlimited; + } diff --git a/src/main/java/com/changolaxtra/cloud/ratelimiter/configuration/StorageManagerConfiguration.java b/src/main/java/com/changolaxtra/cloud/ratelimiter/configuration/StorageManagerConfiguration.java index 7f83d9b..4b51a27 100644 --- a/src/main/java/com/changolaxtra/cloud/ratelimiter/configuration/StorageManagerConfiguration.java +++ b/src/main/java/com/changolaxtra/cloud/ratelimiter/configuration/StorageManagerConfiguration.java @@ -1,6 +1,6 @@ package com.changolaxtra.cloud.ratelimiter.configuration; -import com.changolaxtra.cloud.ratelimiter.data.ApiLimitDataRoot; +import com.changolaxtra.cloud.ratelimiter.core.PlanLimitDataRoot; import lombok.extern.slf4j.Slf4j; import org.eclipse.store.storage.embedded.types.EmbeddedStorage; import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; @@ -15,7 +15,7 @@ public class StorageManagerConfiguration { @Bean public EmbeddedStorageManager.Default embeddedStorageManager() { final EmbeddedStorageManager.Default storageManager = - (EmbeddedStorageManager.Default) EmbeddedStorage.start(new ApiLimitDataRoot()); + (EmbeddedStorageManager.Default) EmbeddedStorage.start(new PlanLimitDataRoot()); storageManager.storeRoot(); return storageManager; } diff --git a/src/main/java/com/changolaxtra/cloud/ratelimiter/core/PlanLimitBucket.java b/src/main/java/com/changolaxtra/cloud/ratelimiter/core/PlanLimitBucket.java new file mode 100644 index 0000000..5ea8986 --- /dev/null +++ b/src/main/java/com/changolaxtra/cloud/ratelimiter/core/PlanLimitBucket.java @@ -0,0 +1,26 @@ +package com.changolaxtra.cloud.ratelimiter.core; + +import com.changolaxtra.cloud.ratelimiter.core.policy.RateLimitPolicy; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class PlanLimitBucket { + + @EqualsAndHashCode.Include + private final String apiKey; + private final TokenBucket tokenBucket; + + public PlanLimitBucket(final RateLimitPolicy rateLimitPolicy) { + this.apiKey = rateLimitPolicy.getApiKey(); + this.tokenBucket = new TokenBucket(rateLimitPolicy); + } + + public String getApiKey() { + return apiKey; + } + + public boolean isAllowed() { + return tokenBucket.isAllowed(); + } + +} diff --git a/src/main/java/com/changolaxtra/cloud/ratelimiter/core/PlanLimitDataRoot.java b/src/main/java/com/changolaxtra/cloud/ratelimiter/core/PlanLimitDataRoot.java new file mode 100644 index 0000000..52d3083 --- /dev/null +++ b/src/main/java/com/changolaxtra/cloud/ratelimiter/core/PlanLimitDataRoot.java @@ -0,0 +1,20 @@ +package com.changolaxtra.cloud.ratelimiter.core; + +import com.changolaxtra.cloud.ratelimiter.core.PlanLimitBucket; +import lombok.Getter; + +import java.util.HashSet; +import java.util.Set; + +@Getter +public class PlanLimitDataRoot { + private final Set planLimitBuckets; + + public PlanLimitDataRoot() { + planLimitBuckets = new HashSet<>(); + } + + public boolean addPlanLimitBucket(final PlanLimitBucket planLimitBucket) { + return planLimitBuckets.add(planLimitBucket); + } +} diff --git a/src/main/java/com/changolaxtra/cloud/ratelimiter/core/TokenBucket.java b/src/main/java/com/changolaxtra/cloud/ratelimiter/core/TokenBucket.java index f6c0042..ba09366 100644 --- a/src/main/java/com/changolaxtra/cloud/ratelimiter/core/TokenBucket.java +++ b/src/main/java/com/changolaxtra/cloud/ratelimiter/core/TokenBucket.java @@ -1,6 +1,6 @@ package com.changolaxtra.cloud.ratelimiter.core; -import com.changolaxtra.cloud.ratelimiter.policy.RateLimitPolicy; +import com.changolaxtra.cloud.ratelimiter.core.policy.RateLimitPolicy; public class TokenBucket { @@ -20,7 +20,7 @@ public TokenBucket(final RateLimitPolicy rateLimitPolicy) { this.refill(); } - public boolean processRequest() { + public boolean isAllowed() { return isUnlimited || processLimitedRequest(); } diff --git a/src/main/java/com/changolaxtra/cloud/ratelimiter/policy/RateLimitPolicy.java b/src/main/java/com/changolaxtra/cloud/ratelimiter/core/policy/RateLimitPolicy.java similarity index 76% rename from src/main/java/com/changolaxtra/cloud/ratelimiter/policy/RateLimitPolicy.java rename to src/main/java/com/changolaxtra/cloud/ratelimiter/core/policy/RateLimitPolicy.java index 43cfde6..0069375 100644 --- a/src/main/java/com/changolaxtra/cloud/ratelimiter/policy/RateLimitPolicy.java +++ b/src/main/java/com/changolaxtra/cloud/ratelimiter/core/policy/RateLimitPolicy.java @@ -1,12 +1,14 @@ -package com.changolaxtra.cloud.ratelimiter.policy; +package com.changolaxtra.cloud.ratelimiter.core.policy; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.ToString; @Getter @Builder @AllArgsConstructor +@ToString public class RateLimitPolicy { private String apiKey; private long allowedRequests; diff --git a/src/main/java/com/changolaxtra/cloud/ratelimiter/data/ApiLimitBucket.java b/src/main/java/com/changolaxtra/cloud/ratelimiter/data/ApiLimitBucket.java deleted file mode 100644 index 6923f07..0000000 --- a/src/main/java/com/changolaxtra/cloud/ratelimiter/data/ApiLimitBucket.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.changolaxtra.cloud.ratelimiter.data; - -import com.changolaxtra.cloud.ratelimiter.core.TokenBucket; -import com.changolaxtra.cloud.ratelimiter.policy.RateLimitPolicy; -import lombok.EqualsAndHashCode; -import lombok.Getter; - -@Getter -@EqualsAndHashCode(onlyExplicitlyIncluded = true) -public class ApiLimitBucket { - - @EqualsAndHashCode.Include - private final String apiKey; - private final TokenBucket tokenBucket; - - public ApiLimitBucket(RateLimitPolicy rateLimitPolicy){ - this.apiKey = rateLimitPolicy.getApiKey(); - this.tokenBucket = new TokenBucket(rateLimitPolicy); - } - -} diff --git a/src/main/java/com/changolaxtra/cloud/ratelimiter/data/ApiLimitDataRoot.java b/src/main/java/com/changolaxtra/cloud/ratelimiter/data/ApiLimitDataRoot.java deleted file mode 100644 index 51b4fbd..0000000 --- a/src/main/java/com/changolaxtra/cloud/ratelimiter/data/ApiLimitDataRoot.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.changolaxtra.cloud.ratelimiter.data; - -import lombok.Getter; - -import java.util.HashSet; -import java.util.Set; - -@Getter -public class ApiLimitDataRoot { - private final Set apiLimitBuckets; - - public ApiLimitDataRoot() { - apiLimitBuckets = new HashSet<>(); - } -} diff --git a/src/main/java/com/changolaxtra/cloud/ratelimiter/exception/RateLimitException.java b/src/main/java/com/changolaxtra/cloud/ratelimiter/exception/RateLimitException.java new file mode 100644 index 0000000..e6b7210 --- /dev/null +++ b/src/main/java/com/changolaxtra/cloud/ratelimiter/exception/RateLimitException.java @@ -0,0 +1,8 @@ +package com.changolaxtra.cloud.ratelimiter.exception; + +public class RateLimitException extends RuntimeException { + + public RateLimitException(final String message) { + super(message); + } +} diff --git a/src/main/java/com/changolaxtra/cloud/ratelimiter/storage/ApiLimitEmbeddedStorage.java b/src/main/java/com/changolaxtra/cloud/ratelimiter/storage/ApiLimitEmbeddedStorage.java deleted file mode 100644 index 4016e15..0000000 --- a/src/main/java/com/changolaxtra/cloud/ratelimiter/storage/ApiLimitEmbeddedStorage.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.changolaxtra.cloud.ratelimiter.storage; - -import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; -import org.springframework.stereotype.Repository; - -@Repository -public class ApiLimitEmbeddedStorage implements ApiLimitStorage { - - private final EmbeddedStorageManager.Default embeddedStorageManager; - - - public ApiLimitEmbeddedStorage(final EmbeddedStorageManager.Default embeddedStorageManager) { - this.embeddedStorageManager = embeddedStorageManager; - } - - -} diff --git a/src/main/java/com/changolaxtra/cloud/ratelimiter/storage/ApiLimitStorage.java b/src/main/java/com/changolaxtra/cloud/ratelimiter/storage/ApiLimitStorage.java deleted file mode 100644 index bc48dbc..0000000 --- a/src/main/java/com/changolaxtra/cloud/ratelimiter/storage/ApiLimitStorage.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.changolaxtra.cloud.ratelimiter.storage; - -public interface ApiLimitStorage { -} diff --git a/src/main/java/com/changolaxtra/cloud/ratelimiter/storage/EmbeddedPlanLimitStorage.java b/src/main/java/com/changolaxtra/cloud/ratelimiter/storage/EmbeddedPlanLimitStorage.java new file mode 100644 index 0000000..b504fa2 --- /dev/null +++ b/src/main/java/com/changolaxtra/cloud/ratelimiter/storage/EmbeddedPlanLimitStorage.java @@ -0,0 +1,41 @@ +package com.changolaxtra.cloud.ratelimiter.storage; + +import com.changolaxtra.cloud.ratelimiter.core.PlanLimitBucket; +import com.changolaxtra.cloud.ratelimiter.core.PlanLimitDataRoot; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager; +import org.springframework.stereotype.Repository; + +import java.util.HashSet; +import java.util.Optional; + +@Repository +public class EmbeddedPlanLimitStorage implements PlanLimitStorage { + + private final EmbeddedStorageManager.Default embeddedStorageManager; + + public EmbeddedPlanLimitStorage(final EmbeddedStorageManager.Default embeddedStorageManager) { + this.embeddedStorageManager = embeddedStorageManager; + } + + @Override + public PlanLimitBucket getPlanLimit(final String apiKey) { + final PlanLimitDataRoot dataRoot = (PlanLimitDataRoot) embeddedStorageManager.root(); + return Optional.ofNullable(dataRoot) + .map(PlanLimitDataRoot::getPlanLimitBuckets) + .orElse(new HashSet<>()) + .stream() + .filter(planLimitBucket -> StringUtils.equalsIgnoreCase(apiKey, planLimitBucket.getApiKey())) + .findFirst() + .orElse(null); + } + + @Override + public boolean saveOrUpdate(final PlanLimitBucket planLimitBucket) { + final PlanLimitDataRoot dataRoot = (PlanLimitDataRoot) embeddedStorageManager.root(); + return Optional.ofNullable(dataRoot) + .map(root -> root.addPlanLimitBucket(planLimitBucket)) + .orElse(false); + } + +} diff --git a/src/main/java/com/changolaxtra/cloud/ratelimiter/storage/PlanLimitStorage.java b/src/main/java/com/changolaxtra/cloud/ratelimiter/storage/PlanLimitStorage.java new file mode 100644 index 0000000..a03fdc4 --- /dev/null +++ b/src/main/java/com/changolaxtra/cloud/ratelimiter/storage/PlanLimitStorage.java @@ -0,0 +1,8 @@ +package com.changolaxtra.cloud.ratelimiter.storage; + +import com.changolaxtra.cloud.ratelimiter.core.PlanLimitBucket; + +public interface PlanLimitStorage { + PlanLimitBucket getPlanLimit(String apiKey); + boolean saveOrUpdate(PlanLimitBucket planLimitBucket); +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..f5acb7e --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,7 @@ +rate-limiter: + apiKey-header-name: "X-API-Key" + default-policy: + apiKey: "" + isUnlimited: false + allowedRequests: 1 + windowSizeInMilliSeconds: 60 diff --git a/src/test/java/com/changolaxtra/cloud/ratelimiter/RateLimiterApplicationTests.java b/src/test/java/com/changolaxtra/cloud/ratelimiter/RateLimiterApplicationTests.java deleted file mode 100644 index caf453f..0000000 --- a/src/test/java/com/changolaxtra/cloud/ratelimiter/RateLimiterApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.changolaxtra.cloud.ratelimiter; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class RateLimiterApplicationTests { - - @Test - void contextLoads() { - } - -}