├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ └── bug_report.md ├── dependabot.yml └── workflows │ ├── greetings.yml │ ├── stale.yml │ └── codeql-analysis.yml ├── spring-cloud-zuul-ratelimit-core └── src │ ├── test │ ├── resources │ │ ├── mockito-extensions │ │ │ └── org.mockito.plugins.MockMaker │ │ └── application-test-duration.yml │ └── java │ │ └── com │ │ └── marcosbarbero │ │ └── cloud │ │ └── autoconfigure │ │ └── zuul │ │ └── ratelimit │ │ ├── config │ │ ├── repository │ │ │ ├── DefaultRateLimiterErrorHandlerTest.java │ │ │ ├── bucket4j │ │ │ │ ├── Bucket4jIgniteRateLimiterTest.java │ │ │ │ ├── Bucket4jJCacheRateLimiterTest.java │ │ │ │ ├── Bucket4jHazelcastRateLimiterTest.java │ │ │ │ └── Bucket4jInfinispanRateLimiterTest.java │ │ │ ├── springdata │ │ │ │ └── JpaRateLimiterTest.java │ │ │ ├── BaseRateLimiterTest.java │ │ │ ├── AbstractRateLimiterTest.java │ │ │ ├── ConsulRateLimiterTest.java │ │ │ └── RedisRateLimiterTest.java │ │ └── properties │ │ │ ├── SecureContextRateLimitTypeTest.java │ │ │ └── RateLimitPropertiesTest.java │ │ ├── support │ │ ├── RateLimitExceededExceptionTest.java │ │ └── StringToMatchTypeConverterTest.java │ │ └── filters │ │ ├── commons │ │ └── TestRouteLocator.java │ │ ├── pre │ │ ├── JpaLimitPreFilterTest.java │ │ ├── RedisRateLimitPreFilterTest.java │ │ └── ConsulRateLimitPreFilterTest.java │ │ └── post │ │ └── RateLimitPostFilterTest.java │ └── main │ ├── resources │ ├── META-INF │ │ └── spring.factories │ └── scripts │ │ └── ratelimit.lua │ └── java │ └── com │ └── marcosbarbero │ └── cloud │ └── autoconfigure │ └── zuul │ └── ratelimit │ ├── support │ ├── RateLimitExceededException.java │ ├── StringToMatchTypeConverter.java │ ├── SecuredRateLimitUtils.java │ ├── RateLimitConstants.java │ ├── RateLimitExceededEvent.java │ ├── DefaultRateLimitUtils.java │ └── DefaultRateLimitKeyGenerator.java │ ├── config │ ├── repository │ │ ├── springdata │ │ │ ├── RateLimiterRepository.java │ │ │ └── JpaRateLimiter.java │ │ ├── RateLimiterErrorHandler.java │ │ ├── AbstractNonBlockCacheRateLimiter.java │ │ ├── DefaultRateLimiterErrorHandler.java │ │ ├── bucket4j │ │ │ ├── Bucket4jJCacheRateLimiter.java │ │ │ ├── Bucket4jIgniteRateLimiter.java │ │ │ ├── Bucket4jHazelcastRateLimiter.java │ │ │ ├── Bucket4jInfinispanRateLimiter.java │ │ │ └── AbstractBucket4jRateLimiter.java │ │ ├── AbstractCacheRateLimiter.java │ │ ├── ConsulRateLimiter.java │ │ ├── AbstractRateLimiter.java │ │ └── RedisRateLimiter.java │ ├── properties │ │ ├── ResponseHeadersVerbosity.java │ │ ├── validators │ │ │ ├── Policies.java │ │ │ └── PoliciesValidator.java │ │ └── RateLimitRepository.java │ ├── RateLimiter.java │ ├── RateLimitKeyGenerator.java │ ├── RateLimitUtils.java │ └── Rate.java │ └── filters │ └── RateLimitPostFilter.java ├── docs └── PULL_REQUEST_TEMPLATE.md ├── spring-cloud-starter-zuul-ratelimit ├── src │ └── main │ │ └── resources │ │ └── META-INF │ │ └── spring.provides └── pom.xml ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ ├── maven-wrapper.properties │ └── MavenWrapperDownloader.java ├── spring-cloud-zuul-ratelimit-tests ├── springdata │ ├── src │ │ ├── main │ │ │ ├── resources │ │ │ │ ├── override-deny-request.properties │ │ │ │ ├── deny-request.properties │ │ │ │ └── application.yml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── marcosbarbero │ │ │ │ └── tests │ │ │ │ └── SpringDataApplication.java │ │ └── test │ │ │ ├── resources │ │ │ └── logback.xml │ │ │ └── java │ │ │ └── com │ │ │ └── marcosbarbero │ │ │ └── tests │ │ │ └── it │ │ │ └── SpringDataDenyOriginTestIT.java │ └── pom.xml ├── consul │ ├── src │ │ ├── test │ │ │ └── resources │ │ │ │ └── logback.xml │ │ └── main │ │ │ ├── resources │ │ │ └── application.yml │ │ │ └── java │ │ │ └── com │ │ │ └── marcosbarbero │ │ │ └── tests │ │ │ └── ConsulApplication.java │ └── pom.xml ├── redis │ ├── src │ │ ├── test │ │ │ └── resources │ │ │ │ └── logback.xml │ │ └── main │ │ │ ├── resources │ │ │ └── application.yml │ │ │ └── java │ │ │ └── com │ │ │ └── marcosbarbero │ │ │ └── tests │ │ │ ├── config │ │ │ └── RedisConfig.java │ │ │ └── RedisApplication.java │ └── pom.xml ├── bucket4j-hazelcast │ ├── src │ │ ├── test │ │ │ └── resources │ │ │ │ └── logback.xml │ │ └── main │ │ │ ├── resources │ │ │ └── application.yml │ │ │ └── java │ │ │ └── com │ │ │ └── marcosbarbero │ │ │ └── tests │ │ │ └── Bucket4jHazelcastApplication.java │ └── pom.xml ├── bucket4j-ignite │ ├── src │ │ ├── test │ │ │ └── resources │ │ │ │ └── logback.xml │ │ └── main │ │ │ ├── resources │ │ │ └── application.yml │ │ │ └── java │ │ │ └── com │ │ │ └── marcosbarbero │ │ │ └── tests │ │ │ └── Bucket4jIgniteApplication.java │ └── pom.xml ├── bucket4j-jcache │ ├── src │ │ ├── test │ │ │ └── resources │ │ │ │ └── logback.xml │ │ └── main │ │ │ ├── resources │ │ │ └── application.properties │ │ │ └── java │ │ │ └── com │ │ │ └── marcosbarbero │ │ │ └── tests │ │ │ └── Bucket4jJCacheApplication.java │ └── pom.xml ├── security-context │ ├── src │ │ ├── test │ │ │ ├── resources │ │ │ │ └── logback.xml │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── marcosbarbero │ │ │ │ └── tests │ │ │ │ └── it │ │ │ │ └── SecurityContextApplicationTestIT.java │ │ └── main │ │ │ ├── resources │ │ │ └── application.yml │ │ │ └── java │ │ │ └── com │ │ │ └── marcosbarbero │ │ │ └── tests │ │ │ └── SecurityContextApplication.java │ └── pom.xml └── bucket4j-infinispan │ ├── src │ ├── test │ │ └── resources │ │ │ └── logback.xml │ └── main │ │ ├── resources │ │ └── application.yml │ │ └── java │ │ └── com │ │ └── marcosbarbero │ │ └── tests │ │ └── Bucket4jInfinispanApplication.java │ └── pom.xml ├── assets └── images │ └── jetbrains_logo.png ├── .ci ├── install.sh ├── deploy.sh └── settings.xml ├── SECURITY.md ├── .travis.yml ├── .gitignore ├── CONTRIBUTING.adoc ├── CODE_OF_CONDUCT.md └── spring-cloud-zuul-ratelimit-dependencies └── pom.xml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: marcosbarbero 4 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /docs/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | #### Changes proposed in this pull request: 4 | - 5 | - 6 | - 7 | 8 | -------------------------------------------------------------------------------- /spring-cloud-starter-zuul-ratelimit/src/main/resources/META-INF/spring.provides: -------------------------------------------------------------------------------- 1 | provides: spring-cloud-zuul-ratelimit-core -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcosbarbero/spring-cloud-zuul-ratelimit/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/springdata/src/main/resources/override-deny-request.properties: -------------------------------------------------------------------------------- 1 | zuul.ratelimit.deny-request.origins[0]= -------------------------------------------------------------------------------- /assets/images/jetbrains_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcosbarbero/spring-cloud-zuul-ratelimit/HEAD/assets/images/jetbrains_logo.png -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/springdata/src/main/resources/deny-request.properties: -------------------------------------------------------------------------------- 1 | zuul.ratelimit.deny-request.origins[0]=127.0.0.1 2 | zuul.ratelimit.deny-request.response-status-code=404 -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.RateLimitAutoConfiguration -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/resources/scripts/ratelimit.lua: -------------------------------------------------------------------------------- 1 | local current = redis.call('incrby', KEYS[1], ARGV[1]) 2 | 3 | if tonumber(current) == tonumber(ARGV[1]) then 4 | redis.call('expire', KEYS[1], ARGV[2]) 5 | end 6 | 7 | return current -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/consul/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/redis/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/springdata/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/bucket4j-hazelcast/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/bucket4j-ignite/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/bucket4j-jcache/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/security-context/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/bucket4j-infinispan/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Make a question to help us to improve our documentation 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the question** 11 | A clear and concise description of what the question is. 12 | 13 | **Additional context** 14 | Add any other context about the problem here. 15 | -------------------------------------------------------------------------------- /.ci/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "[START GPG] Setup Signing Key" 4 | 5 | if [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ] && [[ "$TRAVIS_COMMIT_MESSAGE" == *"[ci deploy]"* ]]; 6 | then 7 | echo $GPG_SECRET_KEYS | base64 --decode | $GPG_EXECUTABLE --import 8 | echo $GPG_OWNERTRUST | base64 --decode | $GPG_EXECUTABLE --import-ownertrust 9 | fi 10 | 11 | echo "[END GPG] Setup Signing Key" -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/resources/application-test-duration.yml: -------------------------------------------------------------------------------- 1 | zuul: 2 | ratelimit: 3 | enabled: true 4 | repository: REDIS 5 | policy-list: 6 | defaultValues: 7 | - limit: 2 8 | withoutUnit: 9 | - quota: PT2S 10 | refresh-interval: 2 11 | withSeconds: 12 | - quota: PT30S 13 | refresh-interval: 30s 14 | withMinutes: 15 | - quota: PT1M 16 | refresh-interval: 1m 17 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ----------------| ------------------ | 7 | | 2.4.x.RELEASE | :white_check_mark: | 8 | | 2.x.x.RELEASE | :white_check_mark: | 9 | | < 2.0 | :x: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | If any security vulnerability is found please open an [issue](https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit/issues) 14 | and tag it with the label `security-vulnerability`. 15 | 16 | For any major security vulnerability it will be fixed in 48 hours since the report. 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | reviewers: 10 | - marcosbarbero 11 | labels: 12 | - dependencies 13 | ignore: 14 | - dependency-name: org.springframework.boot:spring-boot-starter-parent 15 | versions: 16 | - "> 2.4.4, < 2.5" 17 | - dependency-name: org.springframework.boot:spring-boot-starter-parent 18 | versions: 19 | - "> 2.4.4, < 3" 20 | - dependency-name: org.springframework.boot:spring-boot-starter-parent 21 | versions: 22 | - ">= 2.4.a, < 2.5" 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [issues, pull_request] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | issue-message: 'Hello @${{ github.actor }}, thanks for getting in touch, we will get back to you asap! *If you have issues in the `1.x.x.RELEASE` line we recommend you to update to the latest version, unfortunately this line is not supported anymore.*' 13 | pr-message: 'Hello @${{ github.actor }}, thanks for your contribution! Please make sure to link the issue that you are trying to solve and also add tests cases to cover the new code. Thanks!' 14 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark and close stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "30 1 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v3 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 1 day' 17 | stale-pr-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 1 day' 18 | stale-issue-label: 'stale' 19 | stale-pr-label: 'stale' 20 | days-before-stale: 30 21 | days-before-close: 1 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Environment & Versioning (please complete the following information):** 24 | - OS: [e.g. Windows 10] 25 | - Spring Boot Version 26 | - Spring Cloud Version 27 | - Spring Cloud Rate Limit Version 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.ci/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Branch: $TRAVIS_BRANCH" 4 | echo "Pull Request? $TRAVIS_PULL_REQUEST" 5 | echo "Commit Message: $TRAVIS_COMMIT_MESSAGE" 6 | 7 | if [ "$TRAVIS_BRANCH" = "master" ] && [ "$TRAVIS_PULL_REQUEST" = "false" ] && [[ "$TRAVIS_COMMIT_MESSAGE" == *"[ci deploy]"* ]]; then 8 | mvn --settings .ci/settings.xml clean deploy -DskipTests -Pdeploy-parent -N 9 | mvn --settings .ci/settings.xml -f spring-cloud-zuul-ratelimit-dependencies/pom.xml clean deploy -DskipTests -Pdeploy 10 | mvn --settings .ci/settings.xml -f spring-cloud-starter-zuul-ratelimit/pom.xml clean deploy -DskipTests -Pdeploy 11 | mvn --settings .ci/settings.xml -f spring-cloud-zuul-ratelimit-core/pom.xml clean deploy -DskipTests -Pdeploy 12 | 13 | echo "New version released" 14 | fi 15 | 16 | echo "Build Finished" -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/support/RateLimitExceededException.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support; 2 | 3 | import com.netflix.zuul.exception.ZuulException; 4 | import org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException; 5 | import org.springframework.http.HttpStatus; 6 | 7 | /** 8 | * @author Liel Chayoun 9 | */ 10 | public class RateLimitExceededException extends ZuulRuntimeException { 11 | 12 | public RateLimitExceededException() { 13 | super(new ZuulException(HttpStatus.TOO_MANY_REQUESTS.toString(), HttpStatus.TOO_MANY_REQUESTS.value(), null)); 14 | } 15 | 16 | public RateLimitExceededException(HttpStatus httpStatus) { 17 | super(new ZuulException(httpStatus.toString(), httpStatus.value(), null)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.ci/settings.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | ossrh 8 | ${env.SONATYPE_USERNAME} 9 | ${env.SONATYPE_PASSWORD} 10 | 11 | 12 | 13 | 14 | 15 | ossrh 16 | 17 | true 18 | 19 | 20 | ${env.GPG_EXECUTABLE} 21 | ${env.GPG_PASSPHRASE} 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | dist: trusty 3 | sudo: false 4 | jdk: 5 | - oraclejdk11 6 | script: 7 | - "./mvnw --settings .ci/settings.xml install -Dmaven.test.skip=true -Dgpg.skip -Dmaven.javadoc.skip=true 8 | -B -V -q" 9 | before_install: 10 | - chmod -R ug+x .ci 11 | - bash .ci/install.sh 12 | install: 13 | - "./mvnw --settings .ci/settings.xml install -Dmaven.test.skip=true -Dgpg.skip -Dmaven.javadoc.skip=true 14 | -B -V -q" 15 | after_success: 16 | - "./mvnw -pl '!spring-cloud-zuul-ratelimit-dependencies' clean org.jacoco:jacoco-maven-plugin:prepare-agent 17 | verify jacoco:report coveralls:report -q -Dlogging.level.root=ERROR" 18 | cache: 19 | directories: 20 | - "$HOME/.m2/repository" 21 | notifications: 22 | email: 23 | - marcos.hgb@gmail.com 24 | - lchayoun@gmail.com 25 | deploy: 26 | - provider: script 27 | script: bash .ci/deploy.sh 28 | skip_cleanup: true 29 | on: 30 | branch: master 31 | repo: marcosbarbero/spring-cloud-zuul-ratelimit 32 | jdk: oraclejdk11 33 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/DefaultRateLimiterErrorHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.junit.jupiter.api.Test; 5 | 6 | public class DefaultRateLimiterErrorHandlerTest { 7 | 8 | private DefaultRateLimiterErrorHandler target; 9 | 10 | @BeforeEach 11 | public void setUp() { 12 | target = new DefaultRateLimiterErrorHandler(); 13 | } 14 | 15 | @Test 16 | public void testHandleSaveErrorShouldNotThrowException() { 17 | target.handleSaveError("key", new Exception()); 18 | } 19 | 20 | @Test 21 | public void testHandleFetchErrorShouldNotThrowException() { 22 | target.handleFetchError("key", new Exception()); 23 | } 24 | 25 | @Test 26 | public void testHandleErrorShouldNotThrowException() { 27 | target.handleError("msg", new Exception()); 28 | } 29 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Java template 2 | *.class 3 | 4 | # Mobile Tools for Java (J2ME) 5 | .mtj.tmp/ 6 | 7 | # Package Files # 8 | *.jar 9 | *.war 10 | *.ear 11 | 12 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 13 | hs_err_pid* 14 | ### Eclipse template 15 | *.pydevproject 16 | .metadata 17 | .gradle 18 | bin/ 19 | tmp/ 20 | *.tmp 21 | *.bak 22 | *.swp 23 | *~.nib 24 | local.properties 25 | .settings/ 26 | .loadpath 27 | target/ 28 | **/target/ 29 | ignite/ 30 | 31 | # Eclipse Core 32 | .project 33 | 34 | # External tool builders 35 | .externalToolBuilders/ 36 | 37 | # Locally stored "Eclipse launch configurations" 38 | *.launch 39 | 40 | # CDT-specific 41 | .cproject 42 | 43 | # JDT-specific (Eclipse Java Development Tools) 44 | .classpath 45 | 46 | # Java annotation processor (APT) 47 | .factorypath 48 | 49 | # PDT-specific 50 | .buildpath 51 | 52 | # sbteclipse plugin 53 | . 54 | 55 | 56 | # TeXlipse plugin 57 | .texlipse 58 | ### JetBrains template 59 | 60 | *.iml 61 | .idea/ 62 | *.DS_Store 63 | 64 | !.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/redis/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | zuul: 2 | routes: 3 | serviceA: 4 | path: /serviceA 5 | url: forward:/ 6 | serviceB: 7 | path: /serviceB 8 | url: forward:/ 9 | serviceC: 10 | path: /serviceC 11 | url: forward:/ 12 | serviceD: 13 | strip-prefix: false 14 | path: /serviceD/** 15 | url: forward:/ 16 | serviceE: 17 | path: /serviceE 18 | url: forward:/ 19 | ratelimit: 20 | enabled: true 21 | repository: REDIS 22 | policy-list: 23 | serviceA: 24 | - limit: 10 25 | refresh-interval: 60 26 | type: 27 | - origin 28 | serviceB: 29 | - limit: 2 30 | refresh-interval: 2 31 | type: 32 | - origin 33 | serviceD: 34 | - limit: 2 35 | refresh-interval: 60 36 | type: 37 | - url 38 | serviceE: 39 | - quota: 1s 40 | refresh-interval: 60s 41 | type: 42 | - origin 43 | strip-prefix: true 44 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/security-context/src/test/java/com/marcosbarbero/tests/it/SecurityContextApplicationTestIT.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.tests.it; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertTrue; 4 | 5 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimitUtils; 6 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.SecuredRateLimitUtils; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.context.ApplicationContext; 11 | 12 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 13 | public class SecurityContextApplicationTestIT { 14 | 15 | @Autowired 16 | private ApplicationContext context; 17 | 18 | @Test 19 | public void securedRateLimitUtils() { 20 | RateLimitUtils rateLimitUtils = context.getBean(RateLimitUtils.class); 21 | assertTrue(rateLimitUtils instanceof SecuredRateLimitUtils); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/security-context/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | zuul: 2 | routes: 3 | serviceA: 4 | path: /serviceA 5 | url: forward:/ 6 | serviceB: 7 | path: /serviceB 8 | url: forward:/ 9 | serviceC: 10 | path: /serviceC 11 | url: forward:/ 12 | serviceD: 13 | strip-prefix: false 14 | path: /serviceD/** 15 | url: forward:/ 16 | serviceE: 17 | path: /serviceE 18 | url: forward:/ 19 | ratelimit: 20 | enabled: true 21 | repository: REDIS 22 | policy-list: 23 | serviceA: 24 | - limit: 10 25 | refresh-interval: 60 26 | type: 27 | - user 28 | serviceB: 29 | - limit: 2 30 | refresh-interval: 2 31 | type: 32 | - origin 33 | serviceD: 34 | - limit: 2 35 | refresh-interval: 60 36 | type: 37 | - url 38 | serviceE: 39 | - quota: 1 40 | refresh-interval: 60 41 | type: 42 | - origin 43 | strip-prefix: true 44 | 45 | spring: 46 | redis: 47 | port: 6380 48 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/consul/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | zuul: 2 | routes: 3 | serviceA: 4 | path: /serviceA 5 | url: forward:/ 6 | serviceB: 7 | path: /serviceB 8 | url: forward:/ 9 | serviceC: 10 | path: /serviceC 11 | url: forward:/ 12 | serviceD: 13 | strip-prefix: false 14 | path: /serviceD/** 15 | url: forward:/ 16 | serviceE: 17 | path: /serviceE 18 | url: forward:/ 19 | ratelimit: 20 | enabled: true 21 | repository: CONSUL 22 | policy-list: 23 | serviceA: 24 | - limit: 10 25 | refresh-interval: 60 26 | type: 27 | - origin 28 | serviceB: 29 | - limit: 2 30 | refresh-interval: 2 31 | type: 32 | - origin 33 | serviceD: 34 | - limit: 2 35 | refresh-interval: 60 36 | type: 37 | - url 38 | serviceE: 39 | - quota: 1s 40 | refresh-interval: 60s 41 | type: 42 | - origin 43 | strip-prefix: true 44 | 45 | logging: 46 | level: 47 | ROOT: error -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/bucket4j-ignite/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | zuul: 2 | routes: 3 | serviceA: 4 | path: /serviceA 5 | url: forward:/ 6 | serviceB: 7 | path: /serviceB 8 | url: forward:/ 9 | serviceC: 10 | path: /serviceC 11 | url: forward:/ 12 | serviceD: 13 | strip-prefix: false 14 | path: /serviceD/** 15 | url: forward:/ 16 | serviceE: 17 | path: /serviceE 18 | url: forward:/ 19 | ratelimit: 20 | enabled: true 21 | repository: BUCKET4J_IGNITE 22 | policy-list: 23 | serviceA: 24 | - limit: 10 25 | refresh-interval: 60 26 | type: 27 | - origin 28 | serviceB: 29 | - limit: 2 30 | refresh-interval: 2 31 | type: 32 | - origin 33 | serviceD: 34 | - limit: 2 35 | refresh-interval: 60 36 | type: 37 | - url 38 | serviceE: 39 | - quota: 1s 40 | refresh-interval: 60s 41 | type: 42 | - origin 43 | strip-prefix: true 44 | 45 | logging: 46 | level: 47 | ROOT: error -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/bucket4j-hazelcast/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | zuul: 2 | routes: 3 | serviceA: 4 | path: /serviceA 5 | url: forward:/ 6 | serviceB: 7 | path: /serviceB 8 | url: forward:/ 9 | serviceC: 10 | path: /serviceC 11 | url: forward:/ 12 | serviceD: 13 | strip-prefix: false 14 | path: /serviceD/** 15 | url: forward:/ 16 | serviceE: 17 | path: /serviceE 18 | url: forward:/ 19 | ratelimit: 20 | enabled: true 21 | repository: BUCKET4J_HAZELCAST 22 | policy-list: 23 | serviceA: 24 | - limit: 10 25 | refresh-interval: 60 26 | type: 27 | - origin 28 | serviceB: 29 | - limit: 2 30 | refresh-interval: 2 31 | type: 32 | - origin 33 | serviceD: 34 | - limit: 2 35 | refresh-interval: 60 36 | type: 37 | - url 38 | serviceE: 39 | - quota: 1s 40 | refresh-interval: 60s 41 | type: 42 | - origin 43 | strip-prefix: true 44 | 45 | logging: 46 | level: 47 | ROOT: error -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/bucket4j-infinispan/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | zuul: 2 | routes: 3 | serviceA: 4 | path: /serviceA 5 | url: forward:/ 6 | serviceB: 7 | path: /serviceB 8 | url: forward:/ 9 | serviceC: 10 | path: /serviceC 11 | url: forward:/ 12 | serviceD: 13 | strip-prefix: false 14 | path: /serviceD/** 15 | url: forward:/ 16 | serviceE: 17 | path: /serviceE 18 | url: forward:/ 19 | ratelimit: 20 | enabled: true 21 | repository: BUCKET4J_INFINISPAN 22 | policy-list: 23 | serviceA: 24 | - limit: 10 25 | refresh-interval: 60 26 | type: 27 | - origin 28 | serviceB: 29 | - limit: 2 30 | refresh-interval: 2 31 | type: 32 | - origin 33 | serviceD: 34 | - limit: 2 35 | refresh-interval: 60 36 | type: 37 | - url 38 | serviceE: 39 | - quota: 1s 40 | refresh-interval: 60s 41 | type: 42 | - origin 43 | strip-prefix: true 44 | 45 | logging: 46 | level: 47 | ROOT: error -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/springdata/RateLimiterRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.springdata; 18 | 19 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.Rate; 20 | 21 | import org.springframework.data.repository.CrudRepository; 22 | 23 | /** 24 | * @author Liel Chayoun 25 | */ 26 | public interface RateLimiterRepository extends CrudRepository { 27 | 28 | } 29 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/RateLimiterErrorHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository; 18 | 19 | /** 20 | * Handles the backend storage errors. 21 | * 22 | * @author Liel Chayoun 23 | */ 24 | public interface RateLimiterErrorHandler { 25 | 26 | void handleSaveError(String key, Exception e); 27 | 28 | void handleFetchError(String key, Exception e); 29 | 30 | void handleError(String msg, Exception e); 31 | 32 | } 33 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/support/RateLimitExceededExceptionTest.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.Mockito.mock; 5 | 6 | import com.netflix.zuul.exception.ZuulException; 7 | import com.netflix.zuul.monitoring.CounterFactory; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.mockito.MockitoAnnotations; 11 | 12 | public class RateLimitExceededExceptionTest { 13 | 14 | private RateLimitExceededException target; 15 | 16 | @BeforeEach 17 | public void setUp() { 18 | CounterFactory counterFactory = mock(CounterFactory.class); 19 | MockitoAnnotations.initMocks(this); 20 | CounterFactory.initialize(counterFactory); 21 | target = new RateLimitExceededException(); 22 | } 23 | 24 | @Test 25 | public void testExceptionInfo() { 26 | Throwable cause = target.getCause(); 27 | assertThat(cause).isInstanceOf(ZuulException.class); 28 | 29 | ZuulException zuulException = (ZuulException) cause; 30 | assertThat(zuulException.getMessage()).contains("429"); 31 | } 32 | } -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/redis/src/main/java/com/marcosbarbero/tests/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.tests.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import redis.embedded.RedisServer; 5 | 6 | import javax.annotation.PostConstruct; 7 | import javax.annotation.PreDestroy; 8 | import java.io.IOException; 9 | import java.net.Socket; 10 | 11 | /** 12 | * Embedded redis configuration. 13 | * 14 | * @author Marcos Barbero 15 | * @since 2017-06-27 16 | */ 17 | @Configuration 18 | public class RedisConfig { 19 | 20 | private static final int DEFAULT_PORT = 6379; 21 | 22 | private RedisServer redisServer; 23 | 24 | private static boolean available(int port) { 25 | try (Socket ignored = new Socket("localhost", port)) { 26 | return false; 27 | } catch (IOException ignored) { 28 | return true; 29 | } 30 | } 31 | 32 | @PostConstruct 33 | public void setUp() throws IOException { 34 | this.redisServer = new RedisServer(DEFAULT_PORT); 35 | if (available(DEFAULT_PORT)) { 36 | this.redisServer.start(); 37 | } 38 | } 39 | 40 | @PreDestroy 41 | public void destroy() { 42 | this.redisServer.stop(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/springdata/src/test/java/com/marcosbarbero/tests/it/SpringDataDenyOriginTestIT.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.tests.it; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.boot.test.web.client.TestRestTemplate; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.test.annotation.DirtiesContext; 9 | import org.springframework.test.context.TestPropertySource; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.springframework.http.HttpStatus.NOT_FOUND; 13 | 14 | @TestPropertySource(locations = "classpath:/deny-request.properties") 15 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 16 | @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) 17 | public class SpringDataDenyOriginTestIT { 18 | 19 | @Autowired 20 | private TestRestTemplate restTemplate; 21 | 22 | @Test 23 | void testDeniedOrigin() { 24 | ResponseEntity response = this.restTemplate.getForEntity("/serviceC", String.class); 25 | assertEquals(NOT_FOUND, response.getStatusCode()); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/bucket4j/Bucket4jIgniteRateLimiterTest.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.bucket4j; 2 | 3 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.BaseRateLimiterTest; 4 | import org.apache.ignite.Ignite; 5 | import org.apache.ignite.Ignition; 6 | import org.junit.jupiter.api.AfterAll; 7 | import org.junit.jupiter.api.AfterEach; 8 | import org.junit.jupiter.api.BeforeAll; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.mockito.MockitoAnnotations; 11 | 12 | public class Bucket4jIgniteRateLimiterTest extends BaseRateLimiterTest { 13 | 14 | private static Ignite ignite; 15 | 16 | @BeforeAll 17 | public static void setUpClass() { 18 | ignite = Ignition.start(); 19 | } 20 | 21 | @BeforeEach 22 | public void setUp() { 23 | MockitoAnnotations.initMocks(this); 24 | target = new Bucket4jIgniteRateLimiter(ignite.createCache("rateLimit")); 25 | } 26 | 27 | @AfterEach 28 | public void tearDown() { 29 | ignite.destroyCache("rateLimit"); 30 | } 31 | 32 | @AfterAll 33 | public static void tearDownClass() { 34 | Ignition.stop(true); 35 | } 36 | } -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/bucket4j/Bucket4jJCacheRateLimiterTest.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.bucket4j; 2 | 3 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.BaseRateLimiterTest; 4 | import org.apache.ignite.Ignite; 5 | import org.apache.ignite.Ignition; 6 | import org.junit.jupiter.api.AfterAll; 7 | import org.junit.jupiter.api.AfterEach; 8 | import org.junit.jupiter.api.BeforeAll; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.mockito.MockitoAnnotations; 11 | 12 | public class Bucket4jJCacheRateLimiterTest extends BaseRateLimiterTest { 13 | 14 | private static Ignite ignite; 15 | 16 | @BeforeAll 17 | public static void setUpClass() { 18 | ignite = Ignition.start(); 19 | } 20 | 21 | @BeforeEach 22 | public void setUp() { 23 | MockitoAnnotations.initMocks(this); 24 | target = new Bucket4jJCacheRateLimiter(ignite.createCache("rateLimit")); 25 | } 26 | 27 | @AfterEach 28 | public void tearDown() { 29 | ignite.destroyCache("rateLimit"); 30 | } 31 | 32 | @AfterAll 33 | public static void tearDownClass() { 34 | Ignition.stop(true); 35 | } 36 | } -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/filters/commons/TestRouteLocator.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters.commons; 2 | 3 | import org.springframework.cloud.netflix.zuul.filters.Route; 4 | import org.springframework.cloud.netflix.zuul.filters.RouteLocator; 5 | 6 | import java.util.Collection; 7 | import java.util.List; 8 | 9 | /** 10 | * @author Marcos Barbero 11 | * @since 2017-06-23 12 | */ 13 | public class TestRouteLocator implements RouteLocator { 14 | 15 | private final Collection ignoredPaths; 16 | private final List routes; 17 | 18 | public TestRouteLocator(Collection ignoredPaths, List routes) { 19 | this.ignoredPaths = ignoredPaths; 20 | this.routes = routes; 21 | } 22 | 23 | @Override 24 | public Collection getIgnoredPaths() { 25 | return this.ignoredPaths; 26 | } 27 | 28 | @Override 29 | public List getRoutes() { 30 | return this.routes; 31 | } 32 | 33 | @Override 34 | public Route getMatchingRoute(String path) { 35 | return this.routes.stream() 36 | .filter(route -> path.startsWith(route.getPrefix())) 37 | .findFirst() 38 | .orElse(null); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/properties/ResponseHeadersVerbosity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties; 17 | 18 | /** 19 | * Enum which define how response headers should be returned to the client. 20 | * 21 | * @author vasilaio 22 | */ 23 | public enum ResponseHeadersVerbosity { 24 | /** 25 | * Don't add rate limit headers to response. 26 | */ 27 | NONE, 28 | /** 29 | * Add rate limit headers to response. 30 | */ 31 | STANDARD, 32 | /** 33 | * Add rate limit headers to response including keys. 34 | */ 35 | VERBOSE 36 | } 37 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/bucket4j/Bucket4jHazelcastRateLimiterTest.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.bucket4j; 2 | 3 | import com.hazelcast.core.Hazelcast; 4 | import com.hazelcast.core.HazelcastInstance; 5 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.BaseRateLimiterTest; 6 | import org.junit.jupiter.api.AfterAll; 7 | import org.junit.jupiter.api.AfterEach; 8 | import org.junit.jupiter.api.BeforeAll; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.mockito.MockitoAnnotations; 11 | 12 | public class Bucket4jHazelcastRateLimiterTest extends BaseRateLimiterTest { 13 | 14 | private static HazelcastInstance hazelcastInstance; 15 | 16 | @BeforeAll 17 | public static void setUpClass() { 18 | hazelcastInstance = Hazelcast.newHazelcastInstance(); 19 | } 20 | 21 | @BeforeEach 22 | public void setUp() { 23 | MockitoAnnotations.initMocks(this); 24 | target = new Bucket4jHazelcastRateLimiter(hazelcastInstance.getMap("rateLimit")); 25 | } 26 | 27 | @AfterEach 28 | public void tearDown() { 29 | hazelcastInstance.getMap("rateLimit").destroy(); 30 | } 31 | 32 | @AfterAll 33 | public static void tearDownClass() { 34 | Hazelcast.shutdownAll(); 35 | } 36 | } -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/AbstractNonBlockCacheRateLimiter.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository; 2 | 3 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.Rate; 4 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimiter; 5 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties; 6 | 7 | import java.time.Duration; 8 | 9 | /** 10 | * @author mohamed fawzy 11 | */ 12 | public abstract class AbstractNonBlockCacheRateLimiter implements RateLimiter { 13 | 14 | @Override 15 | public Rate consume(RateLimitProperties.Policy policy, String key, Long requestTime) { 16 | final Duration refreshInterval = policy.getRefreshInterval(); 17 | final Long quota = policy.getQuota() != null ? policy.getQuota().toMillis() : null; 18 | final Rate rate = new Rate(key, policy.getLimit(), quota, null, null); 19 | 20 | calcRemainingLimit(policy.getLimit(), refreshInterval, requestTime, key, rate); 21 | calcRemainingQuota(quota, refreshInterval, requestTime, key, rate); 22 | 23 | return rate; 24 | } 25 | 26 | protected abstract void calcRemainingLimit(Long limit, Duration refreshInterval, Long requestTime, String key, Rate rate); 27 | 28 | protected abstract void calcRemainingQuota(Long quota, Duration refreshInterval, Long requestTime, String key, Rate rate); 29 | } 30 | 31 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/properties/validators/Policies.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.validators; 18 | 19 | import java.lang.annotation.ElementType; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.RetentionPolicy; 22 | import java.lang.annotation.Target; 23 | import javax.validation.Constraint; 24 | import javax.validation.Payload; 25 | 26 | /** 27 | * @author Liel Chayoun 28 | */ 29 | @Target({ElementType.FIELD}) 30 | @Retention(RetentionPolicy.RUNTIME) 31 | @Constraint(validatedBy = PoliciesValidator.class) 32 | public @interface Policies { 33 | 34 | String message() default "Policy must contain limit, quota or both"; 35 | 36 | Class[] groups() default {}; 37 | 38 | Class[] payload() default {}; 39 | } 40 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/RateLimiter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config; 18 | 19 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy; 20 | 21 | /** 22 | * @author Marcos Barbero 23 | * @author Liel Chayoun 24 | */ 25 | public interface RateLimiter { 26 | 27 | String QUOTA_SUFFIX = "-quota"; 28 | 29 | /** 30 | * @param policy Template for which rates should be created in case there's no rate limit associated with the 31 | * key 32 | * @param key Unique key that identifies a request 33 | * @param requestTime The total time it took to handle the request 34 | * @return a view of a user's rate request limit 35 | */ 36 | Rate consume(Policy policy, String key, Long requestTime); 37 | } 38 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/bucket4j-jcache/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | zuul.routes.serviceA.path=/serviceA 2 | zuul.routes.serviceA.url=forward:/ 3 | 4 | zuul.routes.serviceB.path=/serviceB 5 | zuul.routes.serviceB.url=forward:/ 6 | 7 | zuul.routes.serviceC.path=/serviceC 8 | zuul.routes.serviceC.url=forward:/ 9 | 10 | zuul.routes.serviceD.path=/serviceD/** 11 | zuul.routes.serviceD.url=forward:/ 12 | zuul.routes.serviceD.strip-prefix=false 13 | 14 | zuul.routes.serviceE.path=/serviceE 15 | zuul.routes.serviceE.url=forward:/ 16 | 17 | zuul.routes.serviceF.path=/serviceF/** 18 | zuul.routes.serviceF.url=forward:/ 19 | zuul.routes.serviceF.strip-prefix=false 20 | 21 | zuul.ratelimit.enabled=true 22 | zuul.ratelimit.repository=BUCKET4J_JCACHE 23 | 24 | zuul.ratelimit.policy-list.serviceA[0].limit=10 25 | zuul.ratelimit.policy-list.serviceA[0].refresh-interval=60 26 | zuul.ratelimit.policy-list.serviceA[0].type[0]=origin 27 | 28 | zuul.ratelimit.policy-list.serviceB[0].limit=2 29 | zuul.ratelimit.policy-list.serviceB[0].refresh-interval=2 30 | zuul.ratelimit.policy-list.serviceB[0].type[0]=origin 31 | 32 | zuul.ratelimit.policy-list.serviceD[0].limit=2 33 | zuul.ratelimit.policy-list.serviceD[0].refresh-interval=60 34 | zuul.ratelimit.policy-list.serviceD[0].type[0]=url 35 | 36 | zuul.ratelimit.policy-list.serviceE[0].quota=1s 37 | zuul.ratelimit.policy-list.serviceE[0].refresh-interval=60s 38 | zuul.ratelimit.policy-list.serviceE[0].type[0]=origin 39 | 40 | zuul.ratelimit.policy-list.serviceF[0].limit=2 41 | zuul.ratelimit.policy-list.serviceF[0].refresh-interval=60 42 | zuul.ratelimit.policy-list.serviceF[0].type[0]=url_pattern=/serviceF/*/specific -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/RateLimitKeyGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config; 18 | 19 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties; 20 | 21 | import org.springframework.cloud.netflix.zuul.filters.Route; 22 | 23 | import javax.servlet.http.HttpServletRequest; 24 | 25 | /** 26 | * Key generator for rate limit control. 27 | * 28 | * @author Liel Chayoun 29 | */ 30 | public interface RateLimitKeyGenerator { 31 | 32 | /** 33 | * Returns a key based on {@link HttpServletRequest}, {@link Route} and 34 | * {@link RateLimitProperties.Policy} 35 | * 36 | * @param request The {@link HttpServletRequest} 37 | * @param route The {@link Route} 38 | * @param policy The {@link RateLimitProperties.Policy} 39 | * @return Generated key 40 | */ 41 | String key(HttpServletRequest request, Route route, RateLimitProperties.Policy policy); 42 | } 43 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/DefaultRateLimiterErrorHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository; 18 | 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | /** 23 | * @author Liel Chayoun 24 | */ 25 | public class DefaultRateLimiterErrorHandler implements RateLimiterErrorHandler { 26 | 27 | private static final Logger log = LoggerFactory.getLogger(DefaultRateLimiterErrorHandler.class); 28 | 29 | @Override 30 | public void handleSaveError(String key, Exception e) { 31 | log.error("Failed saving rate for " + key + ", returning unsaved rate", e); 32 | } 33 | 34 | @Override 35 | public void handleFetchError(String key, Exception e) { 36 | log.error("Failed retrieving rate for " + key + ", will create new rate", e); 37 | } 38 | 39 | @Override 40 | public void handleError(String msg, Exception e) { 41 | log.error(msg, e); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/properties/RateLimitRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties; 18 | 19 | /** 20 | * Enum with all the repositories storage supported. 21 | * 22 | * @author Marcos Barbero 23 | * @author Liel Chayoun 24 | */ 25 | public enum RateLimitRepository { 26 | /** 27 | * Uses Redis as data storage 28 | */ 29 | REDIS, 30 | 31 | /** 32 | * Uses Consul as data storage 33 | */ 34 | CONSUL, 35 | 36 | /** 37 | * Uses SQL database as data storage 38 | */ 39 | JPA, 40 | 41 | /** 42 | * Uses Bucket4j JCache as data storage 43 | */ 44 | BUCKET4J_JCACHE, 45 | 46 | /** 47 | * Uses Bucket4j Hazelcast as data storage 48 | */ 49 | BUCKET4J_HAZELCAST, 50 | 51 | /** 52 | * Uses Bucket4j Ignite as data storage 53 | */ 54 | BUCKET4J_IGNITE, 55 | 56 | /** 57 | * Uses Bucket4j Infinispan as data storage 58 | */ 59 | BUCKET4J_INFINISPAN, 60 | } 61 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/RateLimitUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config; 18 | 19 | import javax.servlet.http.HttpServletRequest; 20 | import java.util.Set; 21 | 22 | /** 23 | * @author Liel Chayoun 24 | */ 25 | public interface RateLimitUtils { 26 | 27 | /** 28 | * Returns the authenticated user from {@link HttpServletRequest}. 29 | * 30 | * @param request The {@link HttpServletRequest} 31 | * @return The authenticated user or annonymous 32 | */ 33 | String getUser(HttpServletRequest request); 34 | 35 | /** 36 | * Returns the remote IP address from {@link HttpServletRequest}. 37 | * 38 | * @param request The {@link HttpServletRequest} 39 | * @return The remote IP address 40 | */ 41 | String getRemoteAddress(HttpServletRequest request); 42 | 43 | /** 44 | * Returns the authenticated user's roles. 45 | * 46 | * @return The authenticated user's roles or empty 47 | */ 48 | Set getUserRoles(); 49 | 50 | } 51 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/support/StringToMatchTypeConverter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support; 18 | 19 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy.MatchType; 20 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitType; 21 | import org.jetbrains.annotations.NotNull; 22 | import org.springframework.core.convert.converter.Converter; 23 | 24 | /** 25 | * @author Liel Chayoun 26 | */ 27 | public final class StringToMatchTypeConverter implements Converter { 28 | 29 | private static final String DELIMITER = "="; 30 | 31 | @Override 32 | public MatchType convert(@NotNull String type) { 33 | if (type.contains(DELIMITER)) { 34 | String[] matchType = type.split(DELIMITER); 35 | return new MatchType(RateLimitType.valueOf(matchType[0].toUpperCase()), matchType[1]); 36 | } 37 | return new MatchType(RateLimitType.valueOf(type.toUpperCase()), null); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/springdata/JpaRateLimiterTest.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.springdata; 2 | 3 | import static org.mockito.ArgumentMatchers.any; 4 | import static org.mockito.Mockito.when; 5 | 6 | import com.google.common.collect.Maps; 7 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.Rate; 8 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.BaseRateLimiterTest; 9 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.RateLimiterErrorHandler; 10 | import java.util.Map; 11 | import java.util.Optional; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.mockito.Mock; 14 | import org.mockito.MockitoAnnotations; 15 | 16 | public class JpaRateLimiterTest extends BaseRateLimiterTest { 17 | 18 | @Mock 19 | private RateLimiterErrorHandler rateLimiterErrorHandler; 20 | @Mock 21 | private RateLimiterRepository rateLimiterRepository; 22 | 23 | @BeforeEach 24 | public void setUp() { 25 | MockitoAnnotations.initMocks(this); 26 | Map repository = Maps.newHashMap(); 27 | when(rateLimiterRepository.save(any(Rate.class))).thenAnswer(invocationOnMock -> { 28 | Rate rate = invocationOnMock.getArgument(0); 29 | repository.put(rate.getKey(), rate); 30 | return rate; 31 | }); 32 | when(rateLimiterRepository.findById(any())).thenAnswer(invocationOnMock -> { 33 | String key = invocationOnMock.getArgument(0); 34 | return Optional.of(repository.get(key)); 35 | }); 36 | 37 | target = new JpaRateLimiter(rateLimiterErrorHandler, rateLimiterRepository); 38 | } 39 | } -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/bucket4j/Bucket4jInfinispanRateLimiterTest.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.bucket4j; 2 | 3 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.BaseRateLimiterTest; 4 | import io.github.bucket4j.grid.GridBucketState; 5 | import org.infinispan.AdvancedCache; 6 | import org.infinispan.configuration.cache.ConfigurationBuilder; 7 | import org.infinispan.functional.FunctionalMap; 8 | import org.infinispan.functional.impl.FunctionalMapImpl; 9 | import org.infinispan.functional.impl.ReadWriteMapImpl; 10 | import org.infinispan.manager.DefaultCacheManager; 11 | import org.junit.jupiter.api.AfterEach; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.mockito.MockitoAnnotations; 14 | 15 | public class Bucket4jInfinispanRateLimiterTest extends BaseRateLimiterTest { 16 | 17 | @BeforeEach 18 | public void setUp() { 19 | MockitoAnnotations.initMocks(this); 20 | DefaultCacheManager cacheManager = new DefaultCacheManager(); 21 | cacheManager.defineConfiguration("rateLimit", new ConfigurationBuilder().build()); 22 | AdvancedCache cache = cacheManager.getCache("rateLimit").getAdvancedCache(); 23 | FunctionalMapImpl functionalMap = FunctionalMapImpl.create(cache); 24 | FunctionalMap.ReadWriteMap readWriteMap = ReadWriteMapImpl.create(functionalMap); 25 | target = new Bucket4jInfinispanRateLimiter(readWriteMap); 26 | } 27 | 28 | @AfterEach 29 | public void tearDown() { 30 | DefaultCacheManager cacheManager = new DefaultCacheManager(); 31 | cacheManager.removeCache("rateLimit"); 32 | } 33 | } -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/support/SecuredRateLimitUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support; 18 | 19 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties; 20 | import org.springframework.security.core.Authentication; 21 | import org.springframework.security.core.authority.AuthorityUtils; 22 | import org.springframework.security.core.context.SecurityContextHolder; 23 | 24 | import java.util.Set; 25 | 26 | import static java.util.Collections.emptySet; 27 | 28 | /** 29 | * @author Marcos Barbero 30 | */ 31 | public class SecuredRateLimitUtils extends DefaultRateLimitUtils { 32 | 33 | public SecuredRateLimitUtils(final RateLimitProperties properties) { 34 | super(properties); 35 | } 36 | 37 | @Override 38 | public Set getUserRoles() { 39 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 40 | if (authentication == null) { 41 | return emptySet(); 42 | } 43 | return AuthorityUtils.authorityListToSet(authentication.getAuthorities()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/bucket4j/Bucket4jJCacheRateLimiter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.bucket4j; 18 | 19 | import io.github.bucket4j.grid.GridBucketState; 20 | import io.github.bucket4j.grid.ProxyManager; 21 | import io.github.bucket4j.grid.jcache.JCache; 22 | import io.github.bucket4j.grid.jcache.JCacheBucketBuilder; 23 | import javax.cache.Cache; 24 | 25 | /** 26 | * Bucket4j rate limiter configuration. 27 | * 28 | * @author Liel Chayoun 29 | * @since 2018-04-06 30 | */ 31 | public class Bucket4jJCacheRateLimiter extends AbstractBucket4jRateLimiter { 32 | 33 | private final Cache cache; 34 | 35 | public Bucket4jJCacheRateLimiter(final Cache cache) { 36 | super(io.github.bucket4j.grid.jcache.JCache.class); 37 | this.cache = cache; 38 | super.init(); 39 | } 40 | 41 | @Override 42 | protected ProxyManager getProxyManager(io.github.bucket4j.grid.jcache.JCache extension) { 43 | return extension.proxyManagerForCache(cache); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/bucket4j/Bucket4jIgniteRateLimiter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.bucket4j; 18 | 19 | import io.github.bucket4j.grid.GridBucketState; 20 | import io.github.bucket4j.grid.ProxyManager; 21 | import io.github.bucket4j.grid.ignite.Ignite; 22 | import io.github.bucket4j.grid.ignite.IgniteBucketBuilder; 23 | import org.apache.ignite.IgniteCache; 24 | 25 | /** 26 | * Bucket4j rate limiter configuration. 27 | * 28 | * @author Liel Chayoun 29 | * @since 2018-04-06 30 | */ 31 | public class Bucket4jIgniteRateLimiter extends AbstractBucket4jRateLimiter { 32 | 33 | private final IgniteCache cache; 34 | 35 | public Bucket4jIgniteRateLimiter(final IgniteCache cache) { 36 | super(io.github.bucket4j.grid.ignite.Ignite.class); 37 | this.cache = cache; 38 | super.init(); 39 | } 40 | 41 | @Override 42 | protected ProxyManager getProxyManager(io.github.bucket4j.grid.ignite.Ignite extension) { 43 | return extension.proxyManagerForCache(cache); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/bucket4j/Bucket4jHazelcastRateLimiter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.bucket4j; 18 | 19 | import com.hazelcast.map.IMap; 20 | import io.github.bucket4j.grid.GridBucketState; 21 | import io.github.bucket4j.grid.ProxyManager; 22 | import io.github.bucket4j.grid.hazelcast.Hazelcast; 23 | import io.github.bucket4j.grid.hazelcast.HazelcastBucketBuilder; 24 | 25 | /** 26 | * Bucket4j rate limiter configuration. 27 | * 28 | * @author Liel Chayoun 29 | * @since 2018-04-06 30 | */ 31 | public class Bucket4jHazelcastRateLimiter extends AbstractBucket4jRateLimiter { 32 | 33 | private final IMap rateLimit; 34 | 35 | public Bucket4jHazelcastRateLimiter(final IMap rateLimit) { 36 | super(io.github.bucket4j.grid.hazelcast.Hazelcast.class); 37 | this.rateLimit = rateLimit; 38 | super.init(); 39 | } 40 | 41 | @Override 42 | protected ProxyManager getProxyManager(io.github.bucket4j.grid.hazelcast.Hazelcast extension) { 43 | return extension.proxyManagerForMap(rateLimit); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/support/RateLimitConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support; 18 | 19 | /** 20 | * @author Marcos Barbero 21 | * @author Liel Chayoun 22 | * @since 2018-04-05 23 | */ 24 | public final class RateLimitConstants { 25 | 26 | private RateLimitConstants() { 27 | } 28 | 29 | // Http Headers Prefix 30 | public static final String HEADER_QUOTA = "X-RateLimit-Quota"; 31 | public static final String HEADER_REMAINING_QUOTA = "X-RateLimit-Remaining-Quota"; 32 | public static final String HEADER_LIMIT = "X-RateLimit-Limit"; 33 | public static final String HEADER_REMAINING = "X-RateLimit-Remaining"; 34 | public static final String HEADER_RESET = "X-RateLimit-Reset"; 35 | 36 | // Request Context Keys 37 | public static final String REQUEST_START_TIME = "rateLimitRequestStartTime"; 38 | public static final String CURRENT_REQUEST_ROUTE = "rateLimitRequestRoute"; 39 | public static final String CURRENT_REQUEST_POLICY = "rateLimitRequestPolicy"; 40 | public static final String RATE_LIMIT_EXCEEDED = "rateLimitExceeded"; 41 | public static final String ALREADY_LIMITED = "rateLimitAlreadyDone"; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/redis/src/main/java/com/marcosbarbero/tests/RedisApplication.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.tests; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.cloud.client.SpringCloudApplication; 5 | import org.springframework.cloud.netflix.zuul.EnableZuulProxy; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | /** 12 | * @author Marcos Barbero 13 | * @since 2017-06-26 14 | */ 15 | @EnableZuulProxy 16 | @SpringCloudApplication 17 | public class RedisApplication { 18 | 19 | public static void main(String... args) { 20 | SpringApplication.run(RedisApplication.class, args); 21 | } 22 | 23 | @RestController 24 | public class ServiceController { 25 | 26 | public static final String RESPONSE_BODY = "ResponseBody"; 27 | 28 | @GetMapping("/serviceA") 29 | public ResponseEntity serviceA() { 30 | return ResponseEntity.ok(RESPONSE_BODY); 31 | } 32 | 33 | @GetMapping("/serviceB") 34 | public ResponseEntity serviceB() { 35 | return ResponseEntity.ok(RESPONSE_BODY); 36 | } 37 | 38 | @GetMapping("/serviceC") 39 | public ResponseEntity serviceC() { 40 | return ResponseEntity.ok(RESPONSE_BODY); 41 | } 42 | 43 | @GetMapping("/serviceD/{paramName}") 44 | public ResponseEntity serviceD(@PathVariable String paramName) { 45 | return ResponseEntity.ok(RESPONSE_BODY + " " + paramName); 46 | } 47 | 48 | @GetMapping("/serviceE") 49 | public ResponseEntity serviceE() throws InterruptedException { 50 | Thread.sleep(1100); 51 | return ResponseEntity.ok(RESPONSE_BODY); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/filters/pre/JpaLimitPreFilterTest.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters.pre; 2 | 3 | import static org.mockito.ArgumentMatchers.any; 4 | import static org.mockito.Mockito.mock; 5 | import static org.mockito.Mockito.when; 6 | 7 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.Rate; 8 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.RateLimiterErrorHandler; 9 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.springdata.JpaRateLimiter; 10 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.springdata.RateLimiterRepository; 11 | import java.util.Map; 12 | import java.util.Optional; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | import org.junit.jupiter.api.BeforeEach; 15 | 16 | /** 17 | * @author Marcos Barbero 18 | * @since 2017-06-30 19 | */ 20 | public class JpaLimitPreFilterTest extends BaseRateLimitPreFilterTest { 21 | 22 | @BeforeEach 23 | @Override 24 | public void setUp() { 25 | Map repository = new ConcurrentHashMap<>(); 26 | RateLimiterRepository rateLimiterRepository = mock(RateLimiterRepository.class); 27 | when(rateLimiterRepository.save(any(Rate.class))).thenAnswer(invocationOnMock -> { 28 | Rate rate = invocationOnMock.getArgument(0); 29 | repository.put(rate.getKey(), rate); 30 | return rate; 31 | }); 32 | when(rateLimiterRepository.findById(any())).thenAnswer(invocationOnMock -> { 33 | String key = invocationOnMock.getArgument(0); 34 | return Optional.of(repository.get(key)); 35 | }); 36 | RateLimiterErrorHandler rateLimiterErrorHandler = mock(RateLimiterErrorHandler.class); 37 | this.setRateLimiter(new JpaRateLimiter(rateLimiterErrorHandler, rateLimiterRepository)); 38 | super.setUp(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/bucket4j/Bucket4jInfinispanRateLimiter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.bucket4j; 18 | 19 | import io.github.bucket4j.grid.GridBucketState; 20 | import io.github.bucket4j.grid.ProxyManager; 21 | import io.github.bucket4j.grid.infinispan.Infinispan; 22 | import io.github.bucket4j.grid.infinispan.InfinispanBucketBuilder; 23 | import org.infinispan.functional.FunctionalMap.ReadWriteMap; 24 | 25 | /** 26 | * Bucket4j rate limiter configuration. 27 | * 28 | * @author Liel Chayoun 29 | * @since 2018-04-06 30 | */ 31 | public class Bucket4jInfinispanRateLimiter extends AbstractBucket4jRateLimiter { 32 | 33 | private final ReadWriteMap readWriteMap; 34 | 35 | public Bucket4jInfinispanRateLimiter(final ReadWriteMap readWriteMap) { 36 | super(io.github.bucket4j.grid.infinispan.Infinispan.class); 37 | this.readWriteMap = readWriteMap; 38 | super.init(); 39 | } 40 | 41 | @Override 42 | protected ProxyManager getProxyManager(io.github.bucket4j.grid.infinispan.Infinispan extension) { 43 | return extension.proxyManagerForMap(readWriteMap); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 8 * * 6' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Setup Java JDK 19 | uses: actions/setup-java@v1.4.3 20 | with: 21 | java-version: 11 22 | - name: Checkout repository 23 | uses: actions/checkout@v2 24 | with: 25 | # We must fetch at least the immediate parents so that if this is 26 | # a pull request then we can checkout the head. 27 | fetch-depth: 2 28 | 29 | # If this run was triggered by a pull request event, then checkout 30 | # the head of the pull request instead of the merge commit. 31 | - run: git checkout HEAD^2 32 | if: ${{ github.event_name == 'pull_request' }} 33 | 34 | # Initializes the CodeQL tools for scanning. 35 | - name: Initialize CodeQL 36 | uses: github/codeql-action/init@v1 37 | # Override language selection by uncommenting this and choosing your languages 38 | # with: 39 | # languages: go, javascript, csharp, python, cpp, java 40 | 41 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 42 | # If this step fails, then you should remove it and run the build manually (see below) 43 | - name: Autobuild 44 | uses: github/codeql-action/autobuild@v1 45 | 46 | # ℹ️ Command-line programs to run using the OS shell. 47 | # 📚 https://git.io/JvXDl 48 | 49 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 50 | # and modify them (or add more) to build your code if your project 51 | # uses a compiled language 52 | 53 | #- run: | 54 | # make bootstrap 55 | # make release 56 | 57 | - name: Perform CodeQL Analysis 58 | uses: github/codeql-action/analyze@v1 59 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/springdata/JpaRateLimiter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.springdata; 18 | 19 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.Rate; 20 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimiter; 21 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.AbstractRateLimiter; 22 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.RateLimiterErrorHandler; 23 | 24 | /** 25 | * JPA {@link RateLimiter} configuration. 26 | * 27 | * @author Marcos Barbero 28 | * @author Liel Chayoun 29 | * @since 2017-06-23 30 | */ 31 | public class JpaRateLimiter extends AbstractRateLimiter { 32 | 33 | private final RateLimiterRepository repository; 34 | 35 | public JpaRateLimiter(final RateLimiterErrorHandler rateLimiterErrorHandler, 36 | final RateLimiterRepository repository) { 37 | super(rateLimiterErrorHandler); 38 | this.repository = repository; 39 | } 40 | 41 | @Override 42 | protected Rate getRate(String key) { 43 | return this.repository.findById(key).orElse(null); 44 | } 45 | 46 | @Override 47 | protected void saveRate(Rate rate) { 48 | this.repository.save(rate); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/BaseRateLimiterTest.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.Rate; 6 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimiter; 7 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy; 8 | import java.time.Duration; 9 | import org.junit.jupiter.api.Test; 10 | 11 | public abstract class BaseRateLimiterTest { 12 | 13 | protected RateLimiter target; 14 | 15 | @Test 16 | public void testConsumeOnlyLimit() { 17 | Policy policy = new Policy(); 18 | policy.setLimit(10L); 19 | policy.setRefreshInterval(Duration.ofSeconds(2)); 20 | 21 | Rate rate = target.consume(policy, "key", null); 22 | assertThat(rate.getRemaining()).isEqualTo(9L); 23 | assertThat(rate.getRemainingQuota()).isNull(); 24 | } 25 | 26 | @Test 27 | public void testConsumeOnlyQuota() { 28 | Policy policy = new Policy(); 29 | policy.setQuota(Duration.ofSeconds(1)); 30 | policy.setRefreshInterval(Duration.ofSeconds(2)); 31 | 32 | Rate rate = target.consume(policy, "key", 800L); 33 | assertThat(rate.getRemainingQuota()).isEqualTo(200L); 34 | assertThat(rate.getRemaining()).isNull(); 35 | } 36 | 37 | @Test 38 | public void testConsume() { 39 | Policy policy = new Policy(); 40 | policy.setLimit(10L); 41 | policy.setQuota(Duration.ofSeconds(1)); 42 | policy.setRefreshInterval(Duration.ofSeconds(2)); 43 | 44 | Rate rate = target.consume(policy, "key", null); 45 | assertThat(rate.getRemaining()).isEqualTo(9L); 46 | assertThat(rate.getRemainingQuota()).isEqualTo(1000L); 47 | 48 | rate = target.consume(policy, "key", 800L); 49 | assertThat(rate.getRemaining()).isEqualTo(9L); 50 | assertThat(rate.getRemainingQuota()).isEqualTo(200L); 51 | } 52 | } -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/AbstractCacheRateLimiter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository; 18 | 19 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.Rate; 20 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimiter; 21 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy; 22 | import java.time.Duration; 23 | 24 | /** 25 | * Bucket4j rate limiter configuration. 26 | * 27 | * @author Liel Chayoun 28 | * @since 2018-04-06 29 | */ 30 | public abstract class AbstractCacheRateLimiter implements RateLimiter { 31 | 32 | @Override 33 | public synchronized Rate consume(Policy policy, String key, Long requestTime) { 34 | final Duration refreshInterval = policy.getRefreshInterval(); 35 | final Long quota = policy.getQuota() != null ? policy.getQuota().toMillis() : null; 36 | final Rate rate = new Rate(key, policy.getLimit(), quota, null, null); 37 | 38 | calcRemainingLimit(policy.getLimit(), refreshInterval, requestTime, key, rate); 39 | calcRemainingQuota(quota, refreshInterval, requestTime, key, rate); 40 | 41 | return rate; 42 | } 43 | 44 | protected abstract void calcRemainingLimit(Long limit, Duration refreshInterval, Long requestTime, String key, Rate rate); 45 | 46 | protected abstract void calcRemainingQuota(Long quota, Duration refreshInterval, Long requestTime, String key, Rate rate); 47 | } 48 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/support/RateLimitExceededEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support; 17 | 18 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy; 19 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters.RateLimitPreFilter; 20 | import java.util.Objects; 21 | import org.springframework.context.ApplicationEvent; 22 | 23 | /** 24 | * Event raised when a rate limit exceeded. 25 | * 26 | * @author vasilaio 27 | */ 28 | public final class RateLimitExceededEvent extends ApplicationEvent { 29 | private static final long serialVersionUID = 5241485625003998587L; 30 | 31 | private final Policy policy; 32 | private final String remoteAddress; 33 | 34 | public RateLimitExceededEvent(RateLimitPreFilter source, Policy policy, String remoteAddress) { 35 | super(source); 36 | this.policy = Objects.requireNonNull(policy, "Policy should not be null."); 37 | this.remoteAddress = Objects.requireNonNull(remoteAddress, "RemoteAddress should not be null."); 38 | } 39 | 40 | /** 41 | * Return the {@link Policy} which raised the event. 42 | * 43 | * @return the {@link Policy} 44 | */ 45 | public Policy getPolicy() { 46 | return this.policy; 47 | } 48 | 49 | /** 50 | * Return the remote IP address. 51 | * 52 | * @return the remote IP address 53 | */ 54 | public String getRemoteAddress() { 55 | return this.remoteAddress; 56 | } 57 | } -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/properties/SecureContextRateLimitTypeTest.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.RateLimitAutoConfiguration; 6 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimitUtils; 7 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.SecuredRateLimitUtils; 8 | import java.util.Collections; 9 | import javax.servlet.http.HttpServletRequest; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | import org.mockito.Mock; 13 | import org.mockito.MockitoAnnotations; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | import org.springframework.cloud.netflix.zuul.filters.Route; 16 | import org.springframework.security.test.context.support.WithMockUser; 17 | 18 | @SpringBootTest(classes = RateLimitAutoConfiguration.class) 19 | public class SecureContextRateLimitTypeTest { 20 | 21 | @Mock 22 | private HttpServletRequest httpServletRequest; 23 | private Route route = new Route("servicea", "/test", "servicea", "/servicea", null, Collections.emptySet()); 24 | private RateLimitUtils rateLimitUtils; 25 | 26 | @BeforeEach 27 | public void setUp() { 28 | MockitoAnnotations.initMocks(this); 29 | RateLimitProperties properties = new RateLimitProperties(); 30 | rateLimitUtils = new SecuredRateLimitUtils(properties); 31 | } 32 | 33 | @Test 34 | @WithMockUser(username = "commonuser", authorities = {"USER"}) 35 | public void applyRole() { 36 | boolean apply = RateLimitType.ROLE.apply(httpServletRequest, route, rateLimitUtils, "user"); 37 | assertThat(apply).isTrue(); 38 | } 39 | 40 | @Test 41 | @WithMockUser(username = "commonuser", authorities = {"ADMIN"}) 42 | public void doNotApplyRole() { 43 | boolean apply = RateLimitType.ROLE.apply(httpServletRequest, route, rateLimitUtils, "user"); 44 | assertThat(apply).isFalse(); 45 | } 46 | 47 | @Test 48 | public void withEmptyAuthentication() { 49 | boolean apply = RateLimitType.ROLE.apply(httpServletRequest, route, rateLimitUtils, "user"); 50 | assertThat(apply).isFalse(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/support/DefaultRateLimitUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support; 18 | 19 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimitUtils; 20 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties; 21 | 22 | import javax.servlet.http.HttpServletRequest; 23 | import java.util.Set; 24 | 25 | import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.X_FORWARDED_FOR_HEADER; 26 | 27 | /** 28 | * @author Liel Chayoun 29 | */ 30 | public class DefaultRateLimitUtils implements RateLimitUtils { 31 | 32 | private static final String ANONYMOUS_USER = "anonymous"; 33 | private static final String X_FORWARDED_FOR_HEADER_DELIMITER = ","; 34 | 35 | private final RateLimitProperties properties; 36 | 37 | public DefaultRateLimitUtils(RateLimitProperties properties) { 38 | this.properties = properties; 39 | } 40 | 41 | @Override 42 | public String getUser(final HttpServletRequest request) { 43 | return request.getRemoteUser() != null ? request.getRemoteUser() : ANONYMOUS_USER; 44 | } 45 | 46 | @Override 47 | public String getRemoteAddress(final HttpServletRequest request) { 48 | String xForwardedFor = request.getHeader(X_FORWARDED_FOR_HEADER); 49 | if (properties.isBehindProxy() && xForwardedFor != null) { 50 | return xForwardedFor.split(X_FORWARDED_FOR_HEADER_DELIMITER)[0].trim(); 51 | } 52 | return request.getRemoteAddr(); 53 | } 54 | 55 | @Override 56 | public Set getUserRoles() { 57 | throw new UnsupportedOperationException("Not supported"); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/bucket4j-hazelcast/src/main/java/com/marcosbarbero/tests/Bucket4jHazelcastApplication.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.tests; 2 | 3 | import com.hazelcast.core.Hazelcast; 4 | import com.hazelcast.map.IMap; 5 | import io.github.bucket4j.grid.GridBucketState; 6 | import org.springframework.beans.factory.annotation.Qualifier; 7 | import org.springframework.boot.SpringApplication; 8 | import org.springframework.cloud.client.SpringCloudApplication; 9 | import org.springframework.cloud.netflix.zuul.EnableZuulProxy; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.PathVariable; 14 | import org.springframework.web.bind.annotation.RestController; 15 | 16 | /** 17 | * @author Liel Chayoun 18 | * @since 2018-04-07 19 | */ 20 | @EnableZuulProxy 21 | @SpringCloudApplication 22 | public class Bucket4jHazelcastApplication { 23 | 24 | public static void main(String... args) { 25 | SpringApplication.run(Bucket4jHazelcastApplication.class, args); 26 | } 27 | 28 | @Bean 29 | @Qualifier("RateLimit") 30 | public IMap map() { 31 | return Hazelcast.newHazelcastInstance().getMap("rateLimit"); 32 | } 33 | 34 | @RestController 35 | public class ServiceController { 36 | 37 | public static final String RESPONSE_BODY = "ResponseBody"; 38 | 39 | @GetMapping("/serviceA") 40 | public ResponseEntity serviceA() { 41 | return ResponseEntity.ok(RESPONSE_BODY); 42 | } 43 | 44 | @GetMapping("/serviceB") 45 | public ResponseEntity serviceB() { 46 | return ResponseEntity.ok(RESPONSE_BODY); 47 | } 48 | 49 | @GetMapping("/serviceC") 50 | public ResponseEntity serviceC() { 51 | return ResponseEntity.ok(RESPONSE_BODY); 52 | } 53 | 54 | @GetMapping("/serviceD/{paramName}") 55 | public ResponseEntity serviceD(@PathVariable String paramName) { 56 | return ResponseEntity.ok(RESPONSE_BODY + " " + paramName); 57 | } 58 | 59 | @GetMapping("/serviceE") 60 | public ResponseEntity serviceE() throws InterruptedException { 61 | Thread.sleep(1100); 62 | return ResponseEntity.ok(RESPONSE_BODY); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/redis/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-cloud-zuul-ratelimit-parent 7 | com.marcosbarbero.cloud 8 | 2.4.3.RELEASE 9 | ../../pom.xml 10 | 11 | 12 | 4.0.0 13 | 14 | redis 15 | Tests - Redis RateLimit 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-data-redis 21 | 22 | 23 | 24 | org.springframework.cloud 25 | spring-cloud-starter-netflix-zuul 26 | 27 | 28 | 29 | com.github.kstyrc 30 | embedded-redis 31 | 0.6 32 | 33 | 34 | 35 | com.marcosbarbero.cloud 36 | spring-cloud-zuul-ratelimit 37 | 38 | 39 | 40 | 41 | 42 | 43 | org.apache.maven.plugins 44 | maven-failsafe-plugin 45 | 2.22.2 46 | 47 | 48 | 49 | integration-test 50 | verify 51 | 52 | 53 | ${skip.tests} 54 | ${argLine} -Duser.timezone=UTC -Xms256m -Xmx256m 55 | 56 | **/*Test* 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/springdata/src/main/java/com/marcosbarbero/tests/SpringDataApplication.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.tests; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.cloud.client.SpringCloudApplication; 5 | import org.springframework.cloud.netflix.zuul.EnableZuulProxy; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | /** 12 | * @author Marcos Barbero 13 | * @since 2017-06-26 14 | */ 15 | @EnableZuulProxy 16 | @SpringCloudApplication 17 | public class SpringDataApplication { 18 | 19 | public static void main(String... args) { 20 | SpringApplication.run(SpringDataApplication.class, args); 21 | } 22 | 23 | 24 | @RestController 25 | public class ServiceController { 26 | 27 | public static final String RESPONSE_BODY = "ResponseBody"; 28 | 29 | @GetMapping("/serviceA") 30 | public ResponseEntity serviceA() { 31 | return ResponseEntity.ok(RESPONSE_BODY); 32 | } 33 | 34 | @GetMapping("/serviceB") 35 | public ResponseEntity serviceB() { 36 | return ResponseEntity.ok(RESPONSE_BODY); 37 | } 38 | 39 | @GetMapping("/serviceC") 40 | public ResponseEntity serviceC() { 41 | return ResponseEntity.ok(RESPONSE_BODY); 42 | } 43 | 44 | @GetMapping("/serviceD/{paramName}") 45 | public ResponseEntity serviceD(@PathVariable String paramName) { 46 | return ResponseEntity.ok(RESPONSE_BODY + " " + paramName); 47 | } 48 | 49 | @GetMapping("/serviceE") 50 | public ResponseEntity serviceE() throws InterruptedException { 51 | Thread.sleep(1100); 52 | return ResponseEntity.ok(RESPONSE_BODY); 53 | } 54 | 55 | @GetMapping("/serviceF") 56 | public ResponseEntity serviceF() { 57 | return ResponseEntity.ok(RESPONSE_BODY); 58 | } 59 | 60 | @GetMapping("/serviceG") 61 | public ResponseEntity serviceG() { 62 | return ResponseEntity.ok(RESPONSE_BODY); 63 | } 64 | 65 | @GetMapping("/serviceH") 66 | public ResponseEntity serviceH() { 67 | return ResponseEntity.ok(RESPONSE_BODY); 68 | } 69 | 70 | @GetMapping("/serviceI") 71 | public ResponseEntity serviceI() { 72 | return ResponseEntity.ok(RESPONSE_BODY); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/support/DefaultRateLimitKeyGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support; 18 | 19 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimitKeyGenerator; 20 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimitUtils; 21 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties; 22 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy; 23 | import org.apache.commons.lang3.StringUtils; 24 | import org.springframework.cloud.netflix.zuul.filters.Route; 25 | 26 | import javax.servlet.http.HttpServletRequest; 27 | import java.util.StringJoiner; 28 | 29 | /** 30 | * Default KeyGenerator implementation. 31 | * 32 | * @author roxspring (github user) 33 | * @author Marcos Barbero 34 | * @author Liel Chayoun 35 | */ 36 | public class DefaultRateLimitKeyGenerator implements RateLimitKeyGenerator { 37 | 38 | private final RateLimitProperties properties; 39 | private final RateLimitUtils rateLimitUtils; 40 | 41 | public DefaultRateLimitKeyGenerator(RateLimitProperties properties, RateLimitUtils rateLimitUtils) { 42 | this.properties = properties; 43 | this.rateLimitUtils = rateLimitUtils; 44 | } 45 | 46 | @Override 47 | public String key(final HttpServletRequest request, final Route route, final Policy policy) { 48 | final StringJoiner joiner = new StringJoiner(":"); 49 | joiner.add(properties.getKeyPrefix()); 50 | if (route != null) { 51 | joiner.add(route.getId()); 52 | } 53 | policy.getType().forEach(matchType -> { 54 | String key = matchType.key(request, route, rateLimitUtils); 55 | if (StringUtils.isNotEmpty(key)) { 56 | joiner.add(key); 57 | } 58 | }); 59 | return joiner.toString(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/AbstractRateLimiterTest.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository; 2 | 3 | import static org.mockito.ArgumentMatchers.any; 4 | import static org.mockito.ArgumentMatchers.matches; 5 | import static org.mockito.Mockito.doThrow; 6 | import static org.mockito.Mockito.verify; 7 | import static org.mockito.Mockito.when; 8 | 9 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy; 10 | import java.lang.reflect.Field; 11 | import java.lang.reflect.Modifier; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.mockito.Mock; 15 | import org.mockito.Mockito; 16 | import org.mockito.MockitoAnnotations; 17 | 18 | public class AbstractRateLimiterTest { 19 | 20 | private AbstractRateLimiter target; 21 | 22 | @Mock 23 | private RateLimiterErrorHandler rateLimiterErrorHandler; 24 | 25 | @BeforeEach 26 | public void setUp() throws Exception { 27 | MockitoAnnotations.initMocks(this); 28 | target = Mockito.mock(AbstractRateLimiter.class, Mockito.CALLS_REAL_METHODS); 29 | Field rateLimiterErrorHandler = AbstractRateLimiter.class.getDeclaredField("rateLimiterErrorHandler"); 30 | boolean accessible = rateLimiterErrorHandler.isAccessible(); 31 | rateLimiterErrorHandler.setAccessible(true); 32 | Field modifiersField = Field.class.getDeclaredField("modifiers"); 33 | modifiersField.setAccessible(true); 34 | modifiersField.setInt(rateLimiterErrorHandler, rateLimiterErrorHandler.getModifiers() & ~Modifier.FINAL); 35 | rateLimiterErrorHandler.set(target, this.rateLimiterErrorHandler); 36 | rateLimiterErrorHandler.setAccessible(accessible); 37 | modifiersField.setInt(rateLimiterErrorHandler, rateLimiterErrorHandler.getModifiers() & Modifier.FINAL); 38 | } 39 | 40 | @Test 41 | public void testConsumeGetRateException() { 42 | when(target.getRate(any())).thenThrow(new RuntimeException()); 43 | Policy policy = new Policy(); 44 | policy.setLimit(100L); 45 | target.consume(policy, "key", 0L); 46 | verify(rateLimiterErrorHandler).handleFetchError(matches("key"), any()); 47 | } 48 | 49 | @Test 50 | public void testConsumeSaveRateException() { 51 | doThrow(new RuntimeException()).when(target).saveRate(any()); 52 | Policy policy = new Policy(); 53 | policy.setLimit(100L); 54 | target.consume(policy, "key", 0L); 55 | verify(rateLimiterErrorHandler).handleSaveError(matches("key"), any()); 56 | } 57 | } -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/properties/validators/PoliciesValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.validators; 18 | 19 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy; 20 | 21 | import javax.validation.ConstraintValidator; 22 | import javax.validation.ConstraintValidatorContext; 23 | import java.util.Collection; 24 | import java.util.Map; 25 | 26 | /** 27 | * Validates the rate limit policies. 28 | * 29 | * @author Liel Chayoun 30 | */ 31 | public class PoliciesValidator implements ConstraintValidator { 32 | 33 | @Override 34 | public void initialize(Policies constraintAnnotation) { 35 | //Nothing to see here, go away 36 | } 37 | 38 | @Override 39 | public boolean isValid(Object value, ConstraintValidatorContext context) { 40 | if (value == null) { 41 | return true; 42 | } else if (value instanceof Collection) { 43 | return isValidCollection((Collection) value); 44 | } else if (value instanceof Map) { 45 | return ((Map) value).values().stream().allMatch(v -> isValid(v, context)); 46 | } else { 47 | return false; 48 | } 49 | } 50 | 51 | private boolean isValidCollection(Collection objects) { 52 | return objects.isEmpty() 53 | || objects.stream().allMatch(this::isValidObject); 54 | } 55 | 56 | private boolean isValidObject(Object o) { 57 | return (o instanceof Policy) && isValidPolicy((Policy) o); 58 | } 59 | 60 | private boolean isValidPolicy(Policy policy) { 61 | return (policy.getLimit() != null || policy.getQuota() != null) && isValid(policy); 62 | } 63 | 64 | private boolean isValid(Policy policy) { 65 | return policy.getType().stream() 66 | .allMatch(type -> type.getType().isValid(type.getMatcher())); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/security-context/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-cloud-zuul-ratelimit-parent 7 | com.marcosbarbero.cloud 8 | 2.4.3.RELEASE 9 | ../../pom.xml 10 | 11 | 12 | 4.0.0 13 | 14 | security-context 15 | Tests - Secured Context RateLimit 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-security 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-data-redis 26 | 27 | 28 | 29 | org.springframework.cloud 30 | spring-cloud-starter-netflix-zuul 31 | 32 | 33 | 34 | com.github.kstyrc 35 | embedded-redis 36 | 0.6 37 | 38 | 39 | 40 | com.marcosbarbero.cloud 41 | spring-cloud-zuul-ratelimit 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.apache.maven.plugins 49 | maven-failsafe-plugin 50 | 2.22.2 51 | 52 | 53 | 54 | integration-test 55 | verify 56 | 57 | 58 | ${skip.tests} 59 | ${argLine} -Duser.timezone=UTC -Xms256m -Xmx256m 60 | 61 | **/*Test* 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/bucket4j-ignite/src/main/java/com/marcosbarbero/tests/Bucket4jIgniteApplication.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.tests; 2 | 3 | import io.github.bucket4j.grid.GridBucketState; 4 | import org.apache.ignite.Ignite; 5 | import org.apache.ignite.IgniteCache; 6 | import org.apache.ignite.Ignition; 7 | import org.apache.ignite.configuration.IgniteConfiguration; 8 | import org.springframework.beans.factory.annotation.Qualifier; 9 | import org.springframework.boot.SpringApplication; 10 | import org.springframework.cloud.client.SpringCloudApplication; 11 | import org.springframework.cloud.netflix.zuul.EnableZuulProxy; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.web.bind.annotation.GetMapping; 15 | import org.springframework.web.bind.annotation.PathVariable; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | import javax.annotation.PreDestroy; 19 | 20 | /** 21 | * @author Liel Chayoun 22 | * @since 2018-04-07 23 | */ 24 | @EnableZuulProxy 25 | @SpringCloudApplication 26 | public class Bucket4jIgniteApplication { 27 | 28 | public static void main(String... args) { 29 | SpringApplication.run(Bucket4jIgniteApplication.class, args); 30 | } 31 | 32 | private Ignite ignite; 33 | 34 | @Bean 35 | @Qualifier("RateLimit") 36 | public IgniteCache cache() { 37 | ignite = Ignition.getOrStart(new IgniteConfiguration()); 38 | return ignite.getOrCreateCache("rateLimit"); 39 | } 40 | 41 | @PreDestroy 42 | public void destroy() { 43 | ignite.destroyCache("rateLimit"); 44 | } 45 | 46 | @RestController 47 | public class ServiceController { 48 | 49 | public static final String RESPONSE_BODY = "ResponseBody"; 50 | 51 | @GetMapping("/serviceA") 52 | public ResponseEntity serviceA() { 53 | return ResponseEntity.ok(RESPONSE_BODY); 54 | } 55 | 56 | @GetMapping("/serviceB") 57 | public ResponseEntity serviceB() { 58 | return ResponseEntity.ok(RESPONSE_BODY); 59 | } 60 | 61 | @GetMapping("/serviceC") 62 | public ResponseEntity serviceC() { 63 | return ResponseEntity.ok(RESPONSE_BODY); 64 | } 65 | 66 | @GetMapping("/serviceD/{paramName}") 67 | public ResponseEntity serviceD(@PathVariable String paramName) { 68 | return ResponseEntity.ok(RESPONSE_BODY + " " + paramName); 69 | } 70 | 71 | @GetMapping("/serviceE") 72 | public ResponseEntity serviceE() throws InterruptedException { 73 | Thread.sleep(1100); 74 | return ResponseEntity.ok(RESPONSE_BODY); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/consul/src/main/java/com/marcosbarbero/tests/ConsulApplication.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.tests; 2 | 3 | import static org.springframework.boot.SpringApplication.run; 4 | 5 | import com.pszymczyk.consul.ConsulProcess; 6 | import com.pszymczyk.consul.ConsulStarterBuilder; 7 | 8 | import javax.annotation.PreDestroy; 9 | 10 | import org.springframework.cloud.client.SpringCloudApplication; 11 | import org.springframework.cloud.netflix.zuul.EnableZuulProxy; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.web.bind.annotation.GetMapping; 16 | import org.springframework.web.bind.annotation.PathVariable; 17 | import org.springframework.web.bind.annotation.RestController; 18 | 19 | /** 20 | * @author Marcos Barbero 21 | * @since 2017-08-28 22 | */ 23 | @EnableZuulProxy 24 | @SpringCloudApplication 25 | public class ConsulApplication { 26 | 27 | public static void main(String... args) { 28 | run(ConsulApplication.class, args); 29 | } 30 | 31 | @RestController 32 | public static class ServiceController { 33 | 34 | public static final String RESPONSE_BODY = "ResponseBody"; 35 | 36 | @GetMapping("/serviceA") 37 | public ResponseEntity serviceA() { 38 | return ResponseEntity.ok(RESPONSE_BODY); 39 | } 40 | 41 | @GetMapping("/serviceB") 42 | public ResponseEntity serviceB() { 43 | return ResponseEntity.ok(RESPONSE_BODY); 44 | } 45 | 46 | @GetMapping("/serviceC") 47 | public ResponseEntity serviceC() { 48 | return ResponseEntity.ok(RESPONSE_BODY); 49 | } 50 | 51 | @GetMapping("/serviceD/{paramName}") 52 | public ResponseEntity serviceD(@PathVariable String paramName) { 53 | return ResponseEntity.ok(RESPONSE_BODY + " " + paramName); 54 | } 55 | 56 | @GetMapping("/serviceE") 57 | public ResponseEntity serviceE() throws InterruptedException { 58 | Thread.sleep(1100); 59 | return ResponseEntity.ok(RESPONSE_BODY); 60 | } 61 | } 62 | 63 | @Configuration 64 | static class ConsulConfig { 65 | 66 | private ConsulProcess consulProcess; 67 | 68 | @Bean 69 | public ConsulProcess consulProcess() { 70 | if (consulProcess == null) { 71 | consulProcess = ConsulStarterBuilder.consulStarter().withHttpPort(8500).build().start(); 72 | } 73 | return consulProcess; 74 | } 75 | 76 | @PreDestroy 77 | public void cleanup() { 78 | consulProcess.close(); 79 | } 80 | 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/consul/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-cloud-zuul-ratelimit-parent 7 | com.marcosbarbero.cloud 8 | 2.4.3.RELEASE 9 | ../../pom.xml 10 | 11 | 4.0.0 12 | 13 | consul 14 | Tests - Consul RateLimit 15 | 16 | 17 | 18 | org.springframework.cloud 19 | spring-cloud-starter-consul 20 | 21 | 22 | 23 | org.springframework.cloud 24 | spring-cloud-starter-netflix-zuul 25 | 26 | 27 | 28 | com.marcosbarbero.cloud 29 | spring-cloud-zuul-ratelimit 30 | 31 | 32 | 33 | com.pszymczyk.consul 34 | embedded-consul 35 | 2.2.1 36 | 37 | 38 | 39 | org.codehaus.groovy 40 | groovy-all 41 | 2.4.21 42 | test 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | org.apache.maven.plugins 51 | maven-failsafe-plugin 52 | 2.22.2 53 | 54 | 55 | 56 | integration-test 57 | verify 58 | 59 | 60 | ${skip.tests} 61 | ${argLine} -Duser.timezone=UTC -Xms256m -Xmx256m 62 | 63 | **/*Test* 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/springdata/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | spring-cloud-zuul-ratelimit-parent 8 | com.marcosbarbero.cloud 9 | 2.4.3.RELEASE 10 | ../../pom.xml 11 | 12 | 13 | 4.0.0 14 | 15 | springdata 16 | Tests - Spring Data RateLimit 17 | 18 | 19 | 20 | com.marcosbarbero.cloud 21 | spring-cloud-zuul-ratelimit 22 | 23 | 24 | 25 | org.springframework.cloud 26 | spring-cloud-starter-netflix-zuul 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-web 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-data-jpa 37 | 38 | 39 | 40 | com.h2database 41 | h2 42 | 43 | 44 | 45 | org.awaitility 46 | awaitility 47 | 4.1.1 48 | test 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | org.apache.maven.plugins 57 | maven-failsafe-plugin 58 | 2.22.2 59 | 60 | 61 | 62 | integration-test 63 | verify 64 | 65 | 66 | ${skip.tests} 67 | ${argLine} -Duser.timezone=UTC -Xms256m -Xmx256m 68 | 69 | **/*Test* 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /CONTRIBUTING.adoc: -------------------------------------------------------------------------------- 1 | = Contributing to Spring Cloud Zuul RateLimit 2 | 3 | Spring Cloud Zuul RateLimit is released under the Apache 2.0 license. If you would like to contribute 4 | something, or simply want to hack on the code this document should help you get started. 5 | 6 | 7 | == Code of Conduct 8 | This project adheres to the Contributor Covenant link:docs/code-of-conduct.adoc[code of 9 | conduct]. By participating, you are expected to uphold this code. Please report 10 | unacceptable behavior to marcos.hgb@gmail.com. 11 | 12 | 13 | == Using GitHub issues 14 | We use GitHub issues to track bugs, enhancements and questions. 15 | 16 | If you are reporting a bug, please help to speed up problem diagnosis by providing as much 17 | information as possible. Ideally, that would include a small sample project that reproduces the 18 | problem. 19 | 20 | 21 | == Code Conventions and Housekeeping 22 | None of these is essential for a pull request, but they will all help. They can also be 23 | added after the original pull request but before a merge. 24 | 25 | * Make sure all new `.java` files to have a simple Javadoc class comment with at least an 26 | `@author` tag identifying you, and preferably at least a paragraph on what the class is 27 | for. 28 | * Add the ASF license header comment to all new `.java` files (copy from existing files 29 | in the project) 30 | * Add yourself as an `@author` to the `.java` files that you modify substantially (more 31 | than cosmetic changes). 32 | * Add some Javadocs. 33 | * A few unit tests would help a lot as well -- someone has to do it. 34 | * If no-one else is using your branch, please rebase it against the current master (or 35 | other target branch in the main project). 36 | * When writing a commit message please follow http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions], 37 | if you are fixing an existing issue please add `Fixes XXXX` at the end of the commit 38 | message (where `XXXX` is the issue number). 39 | 40 | 41 | 42 | == Working with the code 43 | If you don't have an IDE preference we would recommend that you use 44 | https://spring.io/tools/sts[Spring Tools Suite] or 45 | http://eclipse.org[Eclipse] when working with the code. We use the 46 | http://eclipse.org/m2e/[M2Eclipse] eclipse plugin for maven support. Other IDEs and tools 47 | should also work without issue. 48 | 49 | 50 | 51 | === Building from source 52 | To build the source you will need to install 53 | http://maven.apache.org/run-maven/index.html[Apache Maven] v3.2.3 or above and JDK 1.8. 54 | 55 | 56 | 57 | ==== Default build 58 | The project can be built from the root directory using the standard maven command: 59 | 60 | [indent=0] 61 | ---- 62 | $ mvn clean install 63 | ---- 64 | 65 | If you are rebuilding often, you might also want to skip the tests and the execution of 66 | checkstyle until you are ready to submit a pull request: 67 | 68 | [indent=0] 69 | ---- 70 | $ mvn clean install -DskipTests -Pfast 71 | ---- 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/bucket4j-jcache/src/main/java/com/marcosbarbero/tests/Bucket4jJCacheApplication.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.tests; 2 | 3 | import io.github.bucket4j.grid.GridBucketState; 4 | import org.apache.ignite.Ignite; 5 | import org.apache.ignite.IgniteCache; 6 | import org.apache.ignite.Ignition; 7 | import org.apache.ignite.configuration.IgniteConfiguration; 8 | import org.springframework.beans.factory.annotation.Qualifier; 9 | import org.springframework.boot.SpringApplication; 10 | import org.springframework.cloud.client.SpringCloudApplication; 11 | import org.springframework.cloud.netflix.zuul.EnableZuulProxy; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.web.bind.annotation.GetMapping; 15 | import org.springframework.web.bind.annotation.PathVariable; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | import javax.annotation.PreDestroy; 19 | 20 | /** 21 | * @author Liel Chayoun 22 | * @since 2018-04-07 23 | */ 24 | @EnableZuulProxy 25 | @SpringCloudApplication 26 | public class Bucket4jJCacheApplication { 27 | 28 | public static void main(String... args) { 29 | SpringApplication.run(Bucket4jJCacheApplication.class, args); 30 | } 31 | 32 | private Ignite ignite; 33 | 34 | @Bean 35 | @Qualifier("RateLimit") 36 | public IgniteCache cache() { 37 | ignite = Ignition.getOrStart(new IgniteConfiguration()); 38 | return ignite.getOrCreateCache("rateLimit"); 39 | } 40 | 41 | @PreDestroy 42 | public void destroy() { 43 | ignite.destroyCache("rateLimit"); 44 | } 45 | 46 | @RestController 47 | public static class ServiceController { 48 | 49 | public static final String RESPONSE_BODY = "ResponseBody"; 50 | 51 | @GetMapping("/serviceA") 52 | public ResponseEntity serviceA() { 53 | return ResponseEntity.ok(RESPONSE_BODY); 54 | } 55 | 56 | @GetMapping("/serviceB") 57 | public ResponseEntity serviceB() { 58 | return ResponseEntity.ok(RESPONSE_BODY); 59 | } 60 | 61 | @GetMapping("/serviceC") 62 | public ResponseEntity serviceC() { 63 | return ResponseEntity.ok(RESPONSE_BODY); 64 | } 65 | 66 | @GetMapping("/serviceD/{paramName}") 67 | public ResponseEntity serviceD(@PathVariable String paramName) { 68 | return ResponseEntity.ok(RESPONSE_BODY + " " + paramName); 69 | } 70 | 71 | @GetMapping("/serviceE") 72 | public ResponseEntity serviceE() throws InterruptedException { 73 | Thread.sleep(1100); 74 | return ResponseEntity.ok(RESPONSE_BODY); 75 | } 76 | 77 | @GetMapping("/serviceF/{paramName}/specific") 78 | public ResponseEntity serviceF(@PathVariable Integer paramName) { 79 | return ResponseEntity.ok(RESPONSE_BODY + " " + paramName); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/Rate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config; 18 | 19 | import com.fasterxml.jackson.annotation.JsonFormat; 20 | 21 | import javax.persistence.Column; 22 | import javax.persistence.Entity; 23 | import javax.persistence.Id; 24 | import java.util.Date; 25 | 26 | /** 27 | * Represents a view of rate limit in a giving time for a user.

limit - How many requests can be executed by the 28 | * user. Maps to X-RateLimit-Limit header remaining - How many requests are still left on the current window. Maps to 29 | * X-RateLimit-Remaining header reset - Epoch when the rate is replenished by limit. Maps to X-RateLimit-Reset header 30 | * 31 | * @author Marcos Barbero 32 | * @author Liel Chayoun 33 | */ 34 | @Entity 35 | public class Rate { 36 | 37 | @Id 38 | @Column(name = "rate_key") 39 | private String key; 40 | private Long remaining; 41 | private Long remainingQuota; 42 | private Long reset; 43 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy HH:mm:ss") 44 | private Date expiration; 45 | 46 | public Rate() { 47 | } 48 | 49 | public Rate(String key, Long remaining, Long remainingQuota, Long reset, Date expiration) { 50 | this.key = key; 51 | this.remaining = remaining; 52 | this.remainingQuota = remainingQuota; 53 | this.reset = reset; 54 | this.expiration = expiration; 55 | } 56 | 57 | public String getKey() { 58 | return key; 59 | } 60 | 61 | public void setKey(String key) { 62 | this.key = key; 63 | } 64 | 65 | public Long getRemaining() { 66 | return remaining; 67 | } 68 | 69 | public void setRemaining(Long remaining) { 70 | this.remaining = remaining; 71 | } 72 | 73 | public Long getRemainingQuota() { 74 | return remainingQuota; 75 | } 76 | 77 | public void setRemainingQuota(Long remainingQuota) { 78 | this.remainingQuota = remainingQuota; 79 | } 80 | 81 | public Long getReset() { 82 | return reset; 83 | } 84 | 85 | public void setReset(Long reset) { 86 | this.reset = reset; 87 | } 88 | 89 | public Date getExpiration() { 90 | return expiration; 91 | } 92 | 93 | public void setExpiration(Date expiration) { 94 | this.expiration = expiration; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/bucket4j-infinispan/src/main/java/com/marcosbarbero/tests/Bucket4jInfinispanApplication.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.tests; 2 | 3 | import io.github.bucket4j.grid.GridBucketState; 4 | import org.infinispan.AdvancedCache; 5 | import org.infinispan.configuration.cache.ConfigurationBuilder; 6 | import org.infinispan.functional.FunctionalMap.ReadWriteMap; 7 | import org.infinispan.functional.impl.FunctionalMapImpl; 8 | import org.infinispan.functional.impl.ReadWriteMapImpl; 9 | import org.infinispan.manager.DefaultCacheManager; 10 | import org.springframework.beans.factory.annotation.Qualifier; 11 | import org.springframework.boot.SpringApplication; 12 | import org.springframework.cloud.client.SpringCloudApplication; 13 | import org.springframework.cloud.netflix.zuul.EnableZuulProxy; 14 | import org.springframework.context.annotation.Bean; 15 | import org.springframework.http.ResponseEntity; 16 | import org.springframework.web.bind.annotation.GetMapping; 17 | import org.springframework.web.bind.annotation.PathVariable; 18 | import org.springframework.web.bind.annotation.RestController; 19 | 20 | /** 21 | * @author Liel Chayoun 22 | * @since 2018-04-07 23 | */ 24 | @EnableZuulProxy 25 | @SpringCloudApplication 26 | public class Bucket4jInfinispanApplication { 27 | 28 | public static void main(String... args) { 29 | SpringApplication.run(Bucket4jInfinispanApplication.class, args); 30 | } 31 | 32 | @Bean 33 | @Qualifier("RateLimit") 34 | public ReadWriteMap map() { 35 | DefaultCacheManager cacheManager = new DefaultCacheManager(); 36 | cacheManager.defineConfiguration("rateLimit", new ConfigurationBuilder().build()); 37 | AdvancedCache cache = cacheManager.getCache("rateLimit").getAdvancedCache(); 38 | FunctionalMapImpl functionalMap = FunctionalMapImpl.create(cache); 39 | return ReadWriteMapImpl.create(functionalMap); 40 | } 41 | 42 | @RestController 43 | public class ServiceController { 44 | 45 | public static final String RESPONSE_BODY = "ResponseBody"; 46 | 47 | @GetMapping("/serviceA") 48 | public ResponseEntity serviceA() { 49 | return ResponseEntity.ok(RESPONSE_BODY); 50 | } 51 | 52 | @GetMapping("/serviceB") 53 | public ResponseEntity serviceB() { 54 | return ResponseEntity.ok(RESPONSE_BODY); 55 | } 56 | 57 | @GetMapping("/serviceC") 58 | public ResponseEntity serviceC() { 59 | return ResponseEntity.ok(RESPONSE_BODY); 60 | } 61 | 62 | @GetMapping("/serviceD/{paramName}") 63 | public ResponseEntity serviceD(@PathVariable String paramName) { 64 | return ResponseEntity.ok(RESPONSE_BODY + " " + paramName); 65 | } 66 | 67 | @GetMapping("/serviceE") 68 | public ResponseEntity serviceE() throws InterruptedException { 69 | Thread.sleep(1100); 70 | return ResponseEntity.ok(RESPONSE_BODY); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/properties/RateLimitPropertiesTest.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.junit.jupiter.api.Assertions.assertAll; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | 7 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy; 8 | import java.time.Duration; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.stream.Stream; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.params.ParameterizedTest; 14 | import org.junit.jupiter.params.provider.Arguments; 15 | import org.junit.jupiter.params.provider.MethodSource; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 18 | import org.springframework.boot.test.context.SpringBootTest; 19 | import org.springframework.test.context.ActiveProfiles; 20 | 21 | @SpringBootTest(classes = RateLimitPropertiesTest.TestConfiguration.class) 22 | @ActiveProfiles("test-duration") 23 | class RateLimitPropertiesTest { 24 | @Autowired 25 | private RateLimitProperties properties; 26 | 27 | @Test 28 | void should_populate_policies() { 29 | Map> policyList = this.properties.getPolicyList(); 30 | assertThat(policyList).containsKeys("defaultValues", "withoutUnit", "withSeconds", "withMinutes"); 31 | assertAll("Should populate policies list.", 32 | () -> assertThat(this.properties.getPolicies("defaultValues")).isNotEmpty(), 33 | () -> assertThat(this.properties.getPolicies("withoutUnit")).isNotEmpty(), 34 | () -> assertThat(this.properties.getPolicies("withSeconds")).isNotEmpty(), 35 | () -> assertThat(this.properties.getPolicies("withMinutes")).isNotEmpty() 36 | ); 37 | } 38 | 39 | @ParameterizedTest(name = "[{index}] Service \"{0}\" should have: refreshInterval = {1} and quota = {2}.") 40 | @MethodSource("refreshIntervalDs") 41 | void should_populate_refreshinterval(String serviceId, Duration expectedRefreshInterval, Duration expectedQuota) { 42 | List policies = this.properties.getPolicies(serviceId); 43 | assertThat(policies).hasSize(1); 44 | assertEquals(policies.get(0).getRefreshInterval(), expectedRefreshInterval); 45 | assertEquals(policies.get(0).getQuota(), expectedQuota); 46 | } 47 | 48 | private static Stream refreshIntervalDs() { 49 | return Stream.of( 50 | Arguments.of("defaultValues", Duration.ofSeconds(60), null), 51 | Arguments.of("withoutUnit", Duration.ofSeconds(2), Duration.ofSeconds(2)), 52 | Arguments.of("withSeconds", Duration.ofSeconds(30), Duration.ofSeconds(30)), 53 | Arguments.of("withMinutes", Duration.ofMinutes(1), Duration.ofMinutes(1)) 54 | ); 55 | } 56 | 57 | @EnableConfigurationProperties(RateLimitProperties.class) 58 | public static class TestConfiguration { 59 | // nothing 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/springdata/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 4 | name: 5 | username: FaaS 6 | password: 7 | type: org.h2.jdbcx.JdbcDataSource 8 | h2: 9 | console: 10 | enabled: true 11 | settings: 12 | web-allow-others: true 13 | jpa: 14 | hibernate: 15 | ddl-auto: create-drop 16 | generate-ddl: false 17 | database-platform: org.hibernate.dialect.H2Dialect 18 | database: H2 19 | show_sql: true 20 | properties: 21 | hibernate.cache.use_second_level_cache: false 22 | hibernate.cache.use_query_cache: false 23 | hibernate.generate_statistics: true 24 | 25 | zuul: 26 | routes: 27 | serviceA: 28 | path: /serviceA 29 | url: forward:/ 30 | serviceB: 31 | path: /serviceB 32 | url: forward:/ 33 | serviceC: 34 | path: /serviceC 35 | url: forward:/ 36 | serviceD: 37 | strip-prefix: false 38 | path: /serviceD/** 39 | url: forward:/ 40 | serviceE: 41 | path: /serviceE 42 | url: forward:/ 43 | serviceF: 44 | path: /serviceF 45 | url: forward:/ 46 | serviceG: 47 | path: /serviceG 48 | url: forward:/ 49 | serviceH: 50 | path: /serviceH 51 | url: forward:/ 52 | serviceI: 53 | path: /serviceI 54 | url: forward:/ 55 | ratelimit: 56 | enabled: true 57 | repository: JPA 58 | policy-list: 59 | serviceA: 60 | - limit: 10 61 | refresh-interval: 60 62 | type: 63 | - origin 64 | serviceB: 65 | - limit: 2 66 | refresh-interval: 2 67 | type: 68 | - origin 69 | serviceD: 70 | - limit: 2 71 | refresh-interval: 60 72 | type: 73 | - url 74 | serviceE: 75 | - quota: 1s 76 | refresh-interval: 60s 77 | type: 78 | - origin 79 | serviceF: 80 | - limit: 2 81 | refresh-interval: 60 82 | type: 83 | - origin=127.0.0.1 84 | breakOnMatch: true 85 | - limit: 1 86 | refresh-interval: 60 87 | type: 88 | - origin 89 | breakOnMatch: true 90 | serviceG: 91 | - limit: 2 92 | refresh-interval: 60 93 | type: 94 | - origin=128.0.0.1 95 | breakOnMatch: true 96 | - limit: 1 97 | refresh-interval: 60 98 | type: 99 | - origin 100 | breakOnMatch: true 101 | serviceH: 102 | - limit: 2 103 | refresh-interval: 60 104 | type: 105 | - origin=127.0.0.0/22 106 | breakOnMatch: true 107 | - limit: 1 108 | refresh-interval: 60 109 | type: 110 | - origin 111 | breakOnMatch: true 112 | serviceI: 113 | - limit: 2 114 | refresh-interval: 60 115 | type: 116 | - origin=126.0.0.0/22 117 | breakOnMatch: true 118 | - limit: 1 119 | refresh-interval: 60 120 | type: 121 | - origin 122 | breakOnMatch: true 123 | strip-prefix: true 124 | 125 | logging: 126 | level: 127 | ROOT: error -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/ConsulRateLimiter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository; 18 | 19 | import com.ecwid.consul.v1.ConsulClient; 20 | import com.ecwid.consul.v1.kv.model.GetValue; 21 | import com.fasterxml.jackson.core.JsonProcessingException; 22 | import com.fasterxml.jackson.databind.ObjectMapper; 23 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.Rate; 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | import java.io.IOException; 28 | 29 | import static org.apache.commons.lang3.StringUtils.isNotBlank; 30 | import static org.springframework.util.StringUtils.hasText; 31 | 32 | /** 33 | * Consul rate limiter configuration. 34 | * 35 | * @author Liel Chayoun 36 | * @author Marcos Barbero 37 | * @author Mohamed Fawzy 38 | * @since 2017-08-15 39 | */ 40 | public class ConsulRateLimiter extends AbstractRateLimiter { 41 | 42 | private static Logger log = LoggerFactory.getLogger(ConsulRateLimiter.class); 43 | 44 | private final ConsulClient consulClient; 45 | private final ObjectMapper objectMapper; 46 | 47 | public ConsulRateLimiter(RateLimiterErrorHandler rateLimiterErrorHandler, 48 | ConsulClient consulClient, ObjectMapper objectMapper) { 49 | super(rateLimiterErrorHandler); 50 | this.consulClient = consulClient; 51 | this.objectMapper = objectMapper; 52 | } 53 | 54 | @Override 55 | protected Rate getRate(final String key) { 56 | Rate rate = null; 57 | GetValue value = this.consulClient.getKVValue(buildValidConsulKey(key)).getValue(); 58 | if (value != null && value.getDecodedValue() != null) { 59 | try { 60 | rate = this.objectMapper.readValue(value.getDecodedValue(), Rate.class); 61 | } catch (IOException e) { 62 | log.error("Failed to deserialize Rate", e); 63 | } 64 | } 65 | return rate; 66 | } 67 | 68 | @Override 69 | protected void saveRate(Rate rate) { 70 | String value = ""; 71 | try { 72 | value = this.objectMapper.writeValueAsString(rate); 73 | } catch (JsonProcessingException e) { 74 | log.error("Failed to serialize Rate", e); 75 | } 76 | 77 | if (hasText(value)) { 78 | this.consulClient.setKVValue(buildValidConsulKey(rate.getKey()), value); 79 | } 80 | } 81 | 82 | // Slash will corrupt Consul Call , to be changed to _ 83 | private String buildValidConsulKey(String key){ 84 | if(isNotBlank(key)){ 85 | return key.replaceAll("/", "_"); 86 | } 87 | return key; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/bucket4j-ignite/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | spring-cloud-zuul-ratelimit-parent 8 | com.marcosbarbero.cloud 9 | 2.4.3.RELEASE 10 | ../../pom.xml 11 | 12 | 13 | 4.0.0 14 | 15 | bucket4j-ignite 16 | Tests - Bucket4j Ignite RateLimit 17 | 18 | 19 | 20 | com.marcosbarbero.cloud 21 | spring-cloud-zuul-ratelimit 22 | 23 | 24 | 25 | org.springframework.cloud 26 | spring-cloud-starter-netflix-zuul 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-web 32 | 33 | 34 | 35 | com.github.vladimir-bukhtoyarov 36 | bucket4j-core 37 | 6.3.0 38 | 39 | 40 | 41 | com.github.vladimir-bukhtoyarov 42 | bucket4j-jcache 43 | 6.3.0 44 | 45 | 46 | 47 | com.github.vladimir-bukhtoyarov 48 | bucket4j-ignite 49 | 6.3.0 50 | 51 | 52 | 53 | javax.cache 54 | cache-api 55 | 1.1.1 56 | 57 | 58 | 59 | org.apache.ignite 60 | ignite-core 61 | 2.11.0 62 | 63 | 64 | 65 | 66 | 67 | 68 | org.apache.maven.plugins 69 | maven-failsafe-plugin 70 | 2.22.2 71 | 72 | 73 | 74 | integration-test 75 | verify 76 | 77 | 78 | ${skip.tests} 79 | ${argLine} -Duser.timezone=UTC -Xms256m -Xmx256m 80 | 81 | **/*Test* 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/bucket4j-jcache/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | spring-cloud-zuul-ratelimit-parent 8 | com.marcosbarbero.cloud 9 | 2.4.3.RELEASE 10 | ../../pom.xml 11 | 12 | 13 | 4.0.0 14 | 15 | bucket4j-jcache 16 | Tests - Bucket4j JCache RateLimit 17 | 18 | 19 | 20 | com.marcosbarbero.cloud 21 | spring-cloud-zuul-ratelimit 22 | 23 | 24 | 25 | org.springframework.cloud 26 | spring-cloud-starter-netflix-zuul 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-web 32 | 33 | 34 | 35 | com.github.vladimir-bukhtoyarov 36 | bucket4j-core 37 | 6.3.0 38 | 39 | 40 | 41 | com.github.vladimir-bukhtoyarov 42 | bucket4j-jcache 43 | 6.3.0 44 | 45 | 46 | 47 | com.github.vladimir-bukhtoyarov 48 | bucket4j-ignite 49 | 6.3.0 50 | 51 | 52 | 53 | javax.cache 54 | cache-api 55 | 1.1.1 56 | 57 | 58 | 59 | org.apache.ignite 60 | ignite-core 61 | 2.11.0 62 | 63 | 64 | 65 | 66 | 67 | 68 | org.apache.maven.plugins 69 | maven-failsafe-plugin 70 | 2.22.2 71 | 72 | 73 | 74 | integration-test 75 | verify 76 | 77 | 78 | ${skip.tests} 79 | ${argLine} -Duser.timezone=UTC -Xms256m -Xmx256m 80 | 81 | **/*Test* 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/bucket4j-hazelcast/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | spring-cloud-zuul-ratelimit-parent 8 | com.marcosbarbero.cloud 9 | 2.4.3.RELEASE 10 | ../../pom.xml 11 | 12 | 13 | 4.0.0 14 | 15 | bucket4j-hazelcast 16 | Tests - Bucket4j Hazelcast RateLimit 17 | 18 | 19 | 20 | com.marcosbarbero.cloud 21 | spring-cloud-zuul-ratelimit 22 | 23 | 24 | 25 | org.springframework.cloud 26 | spring-cloud-starter-netflix-zuul 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-web 32 | 33 | 34 | 35 | com.github.vladimir-bukhtoyarov 36 | bucket4j-core 37 | 6.3.0 38 | 39 | 40 | 41 | com.github.vladimir-bukhtoyarov 42 | bucket4j-jcache 43 | 6.3.0 44 | 45 | 46 | 47 | com.github.vladimir-bukhtoyarov 48 | bucket4j-hazelcast 49 | 6.3.0 50 | 51 | 52 | 53 | javax.cache 54 | cache-api 55 | 1.1.1 56 | 57 | 58 | 59 | com.hazelcast 60 | hazelcast 61 | 5.1 62 | 63 | 64 | 65 | 66 | 67 | 68 | org.apache.maven.plugins 69 | maven-failsafe-plugin 70 | 2.22.2 71 | 72 | 73 | 74 | integration-test 75 | verify 76 | 77 | 78 | ${skip.tests} 79 | ${argLine} -Duser.timezone=UTC -Xms256m -Xmx256m 80 | 81 | **/*Test* 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at marcos.hgb@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/ConsulRateLimiterTest.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.ArgumentMatchers.any; 5 | import static org.mockito.ArgumentMatchers.anyString; 6 | import static org.mockito.ArgumentMatchers.eq; 7 | import static org.mockito.Mockito.verifyNoInteractions; 8 | import static org.mockito.Mockito.when; 9 | 10 | import com.ecwid.consul.v1.ConsulClient; 11 | import com.ecwid.consul.v1.Response; 12 | import com.ecwid.consul.v1.kv.model.GetValue; 13 | import com.fasterxml.jackson.core.JsonProcessingException; 14 | import com.fasterxml.jackson.databind.ObjectMapper; 15 | import com.google.common.collect.Maps; 16 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.Rate; 17 | import java.io.IOException; 18 | import java.util.Base64; 19 | import java.util.Map; 20 | import org.junit.jupiter.api.BeforeEach; 21 | import org.junit.jupiter.api.Test; 22 | import org.mockito.Mock; 23 | import org.mockito.Mockito; 24 | import org.mockito.MockitoAnnotations; 25 | 26 | public class ConsulRateLimiterTest extends BaseRateLimiterTest { 27 | 28 | @Mock 29 | private RateLimiterErrorHandler rateLimiterErrorHandler; 30 | @Mock 31 | private ConsulClient consulClient; 32 | @Mock 33 | private ObjectMapper objectMapper; 34 | 35 | @BeforeEach 36 | public void setUp() { 37 | MockitoAnnotations.initMocks(this); 38 | Map repository = Maps.newHashMap(); 39 | when(consulClient.setKVValue(any(), any())).thenAnswer(invocation -> { 40 | String key = invocation.getArgument(0); 41 | String value = invocation.getArgument(1); 42 | repository.put(key, value); 43 | return null; 44 | }); 45 | when(consulClient.getKVValue(any())).thenAnswer(invocation -> { 46 | String key = invocation.getArgument(0); 47 | GetValue getValue = new GetValue(); 48 | String value = repository.get(key); 49 | getValue.setValue(value != null ? Base64.getEncoder().encodeToString(value.getBytes()) : null); 50 | return new Response<>(getValue, 1L, true, 1L); 51 | }); 52 | ObjectMapper objectMapper = new ObjectMapper(); 53 | target = new ConsulRateLimiter(rateLimiterErrorHandler, consulClient, objectMapper); 54 | } 55 | 56 | @Test 57 | public void testGetRateException() throws IOException { 58 | GetValue getValue = new GetValue(); 59 | getValue.setValue(""); 60 | when(consulClient.getKVValue(any())).thenReturn(new Response<>(getValue, 1L, true, 1L)); 61 | when(objectMapper.readValue(anyString(), eq(Rate.class))).thenAnswer(invocation -> { 62 | throw new IOException(); 63 | }); 64 | ConsulRateLimiter consulRateLimiter = new ConsulRateLimiter(rateLimiterErrorHandler, consulClient, objectMapper); 65 | 66 | Rate rate = consulRateLimiter.getRate(""); 67 | assertThat(rate).isNull(); 68 | } 69 | 70 | @Test 71 | public void testSaveRateException() throws IOException { 72 | JsonProcessingException jsonProcessingException = Mockito.mock(JsonProcessingException.class); 73 | when(objectMapper.writeValueAsString(any())).thenThrow(jsonProcessingException); 74 | ConsulRateLimiter consulRateLimiter = new ConsulRateLimiter(rateLimiterErrorHandler, consulClient, objectMapper); 75 | 76 | consulRateLimiter.saveRate(null); 77 | verifyNoInteractions(consulClient); 78 | } 79 | } -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/bucket4j-infinispan/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | spring-cloud-zuul-ratelimit-parent 8 | com.marcosbarbero.cloud 9 | 2.4.3.RELEASE 10 | ../../pom.xml 11 | 12 | 13 | 4.0.0 14 | 15 | bucket4j-infinispan 16 | Tests - Bucket4j Infinispan RateLimit 17 | 18 | 19 | 20 | com.marcosbarbero.cloud 21 | spring-cloud-zuul-ratelimit 22 | 23 | 24 | 25 | org.springframework.cloud 26 | spring-cloud-starter-netflix-zuul 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-web 32 | 33 | 34 | 35 | com.github.vladimir-bukhtoyarov 36 | bucket4j-core 37 | 6.3.0 38 | 39 | 40 | 41 | com.github.vladimir-bukhtoyarov 42 | bucket4j-jcache 43 | 6.3.0 44 | 45 | 46 | 47 | com.github.vladimir-bukhtoyarov 48 | bucket4j-infinispan 49 | 6.3.0 50 | 51 | 52 | 53 | javax.cache 54 | cache-api 55 | 1.1.1 56 | 57 | 58 | 59 | org.infinispan 60 | infinispan-core 61 | 13.0.2.Final 62 | 63 | 64 | 65 | org.infinispan 66 | infinispan-commons 67 | 13.0.2.Final 68 | 69 | 70 | 71 | 72 | 73 | 74 | org.apache.maven.plugins 75 | maven-failsafe-plugin 76 | 2.22.2 77 | 78 | 79 | 80 | integration-test 81 | verify 82 | 83 | 84 | ${skip.tests} 85 | ${argLine} -Duser.timezone=UTC -Xms256m -Xmx256m 86 | 87 | **/*Test* 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/support/StringToMatchTypeConverterTest.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy.MatchType; 6 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitType; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | 10 | public class StringToMatchTypeConverterTest { 11 | 12 | private StringToMatchTypeConverter target; 13 | 14 | @BeforeEach 15 | public void setUp() { 16 | target = new StringToMatchTypeConverter(); 17 | } 18 | 19 | @Test 20 | public void testConvertStringTypeOnly() { 21 | MatchType matchType = target.convert("url"); 22 | assertThat(matchType).isNotNull(); 23 | assertThat(matchType.getType()).isEqualByComparingTo(RateLimitType.URL); 24 | assertThat(matchType.getMatcher()).isNull(); 25 | } 26 | 27 | @Test 28 | public void testConvertStringTypeUrlPatternWithMatcher() { 29 | MatchType matchType = target.convert("url_pattern=/api/*/specific"); 30 | assertThat(matchType).isNotNull(); 31 | assertThat(matchType.getType()).isEqualByComparingTo(RateLimitType.URL_PATTERN); 32 | assertThat(matchType.getMatcher()).isEqualTo("/api/*/specific"); 33 | } 34 | 35 | @Test 36 | public void testConvertStringTypeWithMatcher() { 37 | MatchType matchType = target.convert("url=/api"); 38 | assertThat(matchType).isNotNull(); 39 | assertThat(matchType.getType()).isEqualByComparingTo(RateLimitType.URL); 40 | assertThat(matchType.getMatcher()).isEqualTo("/api"); 41 | } 42 | 43 | @Test 44 | @SuppressWarnings("deprecation") 45 | public void testConvertStringTypeMethodOnly() { 46 | MatchType matchType = target.convert("httpmethod"); 47 | assertThat(matchType).isNotNull(); 48 | assertThat(matchType.getType()).isEqualByComparingTo(RateLimitType.HTTPMETHOD); 49 | assertThat(matchType.getMatcher()).isNull(); 50 | } 51 | 52 | @Test 53 | @SuppressWarnings("deprecation") 54 | public void testConvertStringTypeMethodWithMatcher() { 55 | MatchType matchType = target.convert("httpmethod=get"); 56 | assertThat(matchType).isNotNull(); 57 | assertThat(matchType.getType()).isEqualByComparingTo(RateLimitType.HTTPMETHOD); 58 | assertThat(matchType.getMatcher()).isEqualTo("get"); 59 | } 60 | 61 | @Test 62 | public void testConvertStringTypeHttpMethodOnly() { 63 | MatchType matchType = target.convert("http_method"); 64 | assertThat(matchType).isNotNull(); 65 | assertThat(matchType.getType()).isEqualByComparingTo(RateLimitType.HTTP_METHOD); 66 | assertThat(matchType.getMatcher()).isNull(); 67 | } 68 | 69 | @Test 70 | public void testConvertStringTypeHttpMethodWithMatcher() { 71 | MatchType matchType = target.convert("http_method=get"); 72 | assertThat(matchType).isNotNull(); 73 | assertThat(matchType.getType()).isEqualByComparingTo(RateLimitType.HTTP_METHOD); 74 | assertThat(matchType.getMatcher()).isEqualTo("get"); 75 | } 76 | 77 | @Test 78 | public void testConvertStringTypeHttpHeaderWithMatcher() { 79 | MatchType matchType = target.convert("http_header=customHeader"); 80 | assertThat(matchType).isNotNull(); 81 | assertThat(matchType.getType()).isEqualByComparingTo(RateLimitType.HTTP_HEADER); 82 | assertThat(matchType.getMatcher()).isEqualTo("customHeader"); 83 | } 84 | } -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/AbstractRateLimiter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository; 18 | 19 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.Rate; 20 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimiter; 21 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy; 22 | import java.util.Date; 23 | 24 | /** 25 | * Abstract implementation for {@link RateLimiter}. 26 | * 27 | * @author Liel Chayoun 28 | * @author Marcos Barbero 29 | * @since 2017-08-28 30 | */ 31 | public abstract class AbstractRateLimiter implements RateLimiter { 32 | 33 | private final RateLimiterErrorHandler rateLimiterErrorHandler; 34 | 35 | protected AbstractRateLimiter(RateLimiterErrorHandler rateLimiterErrorHandler) { 36 | this.rateLimiterErrorHandler = rateLimiterErrorHandler; 37 | } 38 | 39 | protected abstract Rate getRate(String key); 40 | 41 | protected abstract void saveRate(Rate rate); 42 | 43 | @Override 44 | public synchronized Rate consume(final Policy policy, final String key, final Long requestTime) { 45 | Rate rate = this.create(policy, key); 46 | updateRate(policy, rate, requestTime); 47 | try { 48 | saveRate(rate); 49 | } catch (RuntimeException e) { 50 | rateLimiterErrorHandler.handleSaveError(key, e); 51 | } 52 | return rate; 53 | } 54 | 55 | private Rate create(final Policy policy, final String key) { 56 | Rate rate = null; 57 | try { 58 | rate = this.getRate(key); 59 | } catch (RuntimeException e) { 60 | rateLimiterErrorHandler.handleFetchError(key, e); 61 | } 62 | 63 | if (!isExpired(rate)) { 64 | return rate; 65 | } 66 | 67 | Long limit = policy.getLimit(); 68 | Long quota = policy.getQuota() != null ? policy.getQuota().toMillis() : null; 69 | long refreshInterval = policy.getRefreshInterval().toMillis(); 70 | Date expiration = new Date(System.currentTimeMillis() + refreshInterval); 71 | 72 | return new Rate(key, limit, quota, refreshInterval, expiration); 73 | } 74 | 75 | private void updateRate(final Policy policy, final Rate rate, final Long requestTime) { 76 | if (rate.getReset() > 0) { 77 | Long reset = rate.getExpiration().getTime() - System.currentTimeMillis(); 78 | rate.setReset(reset); 79 | } 80 | if (policy.getLimit() != null && requestTime == null) { 81 | rate.setRemaining(Math.max(-1, rate.getRemaining() - 1)); 82 | } 83 | if (policy.getQuota() != null && requestTime != null) { 84 | rate.setRemainingQuota(Math.max(-1, rate.getRemainingQuota() - requestTime)); 85 | } 86 | } 87 | 88 | private boolean isExpired(final Rate rate) { 89 | return rate == null || (rate.getExpiration().getTime() < System.currentTimeMillis()); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/filters/RateLimitPostFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters; 18 | 19 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimitKeyGenerator; 20 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimitUtils; 21 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimiter; 22 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties; 23 | import com.netflix.zuul.context.RequestContext; 24 | import org.springframework.cloud.netflix.zuul.filters.Route; 25 | import org.springframework.cloud.netflix.zuul.filters.RouteLocator; 26 | import org.springframework.web.util.UrlPathHelper; 27 | 28 | import javax.servlet.http.HttpServletRequest; 29 | 30 | import static com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.RateLimitConstants.REQUEST_START_TIME; 31 | import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.POST_TYPE; 32 | 33 | /** 34 | * @author Marcos Barbero 35 | * @author Liel Chayoun 36 | */ 37 | public class RateLimitPostFilter extends AbstractRateLimitFilter { 38 | 39 | private final RateLimiter rateLimiter; 40 | private final RateLimitKeyGenerator rateLimitKeyGenerator; 41 | 42 | public RateLimitPostFilter(final RateLimitProperties properties, final RouteLocator routeLocator, 43 | final UrlPathHelper urlPathHelper, final RateLimiter rateLimiter, 44 | final RateLimitKeyGenerator rateLimitKeyGenerator, final RateLimitUtils rateLimitUtils) { 45 | super(properties, routeLocator, urlPathHelper, rateLimitUtils); 46 | this.rateLimiter = rateLimiter; 47 | this.rateLimitKeyGenerator = rateLimitKeyGenerator; 48 | } 49 | 50 | @Override 51 | public String filterType() { 52 | return POST_TYPE; 53 | } 54 | 55 | @Override 56 | public int filterOrder() { 57 | return properties.getPostFilterOrder(); 58 | } 59 | 60 | @Override 61 | public boolean shouldFilter() { 62 | return super.shouldFilter() && getRequestStartTime() != null; 63 | } 64 | 65 | private Long getRequestStartTime() { 66 | final RequestContext ctx = RequestContext.getCurrentContext(); 67 | final HttpServletRequest request = ctx.getRequest(); 68 | return (Long) request.getAttribute(REQUEST_START_TIME); 69 | } 70 | 71 | @Override 72 | public Object run() { 73 | RequestContext ctx = RequestContext.getCurrentContext(); 74 | HttpServletRequest request = ctx.getRequest(); 75 | Route route = route(request); 76 | 77 | policy(route, request).forEach(policy -> { 78 | long requestTime = System.currentTimeMillis() - getRequestStartTime(); 79 | String key = rateLimitKeyGenerator.key(request, route, policy); 80 | rateLimiter.consume(policy, key, requestTime > 0 ? requestTime : 1); 81 | }); 82 | 83 | return null; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/filters/pre/RedisRateLimitPreFilterTest.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters.pre; 2 | 3 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.RateLimiterErrorHandler; 4 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.RedisRateLimiter; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.data.redis.core.StringRedisTemplate; 8 | 9 | import java.util.Objects; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | import static com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.RateLimitConstants.HEADER_REMAINING; 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | import static org.junit.jupiter.api.Assertions.assertTrue; 15 | import static org.mockito.ArgumentMatchers.*; 16 | import static org.mockito.Mockito.doReturn; 17 | import static org.mockito.Mockito.mock; 18 | 19 | /** 20 | * @author Marcos Barbero 21 | * @since 2017-06-30 22 | */ 23 | public class RedisRateLimitPreFilterTest extends BaseRateLimitPreFilterTest { 24 | 25 | private StringRedisTemplate redisTemplate; 26 | 27 | @BeforeEach 28 | @Override 29 | public void setUp() { 30 | redisTemplate = mock(StringRedisTemplate.class); 31 | RateLimiterErrorHandler rateLimiterErrorHandler = mock(RateLimiterErrorHandler.class); 32 | this.setRateLimiter(new RedisRateLimiter(rateLimiterErrorHandler, this.redisTemplate)); 33 | super.setUp(); 34 | } 35 | 36 | @Test 37 | @Override 38 | @SuppressWarnings("unchecked") 39 | public void testRateLimitExceedCapacity() throws Exception { 40 | doReturn(3L) 41 | .when(redisTemplate).execute(any(), anyList(), anyString(), anyString()); 42 | 43 | super.testRateLimitExceedCapacity(); 44 | } 45 | 46 | @Test 47 | @Override 48 | @SuppressWarnings("unchecked") 49 | public void testRateLimit() throws Exception { 50 | doReturn(1L, 2L) 51 | .when(redisTemplate).execute(any(), anyList(), anyString(), anyString()); 52 | 53 | 54 | this.request.setRequestURI("/serviceA"); 55 | this.request.setRemoteAddr("10.0.0.100"); 56 | 57 | assertTrue(this.filter.shouldFilter()); 58 | 59 | for (int i = 0; i < 2; i++) { 60 | this.filter.run(); 61 | } 62 | 63 | String key = "-null_serviceA_10.0.0.100_anonymous"; 64 | String remaining = this.response.getHeader(HEADER_REMAINING + key); 65 | assertEquals("0", remaining); 66 | 67 | TimeUnit.SECONDS.sleep(2); 68 | 69 | doReturn(1L) 70 | .when(redisTemplate).execute(any(), anyList(), anyString(), anyString()); 71 | 72 | this.filter.run(); 73 | remaining = this.response.getHeader(HEADER_REMAINING + key); 74 | assertEquals("1", remaining); 75 | } 76 | 77 | @Test 78 | public void testShouldReturnCorrectRateRemainingValue() { 79 | doReturn(1L, 2L) 80 | .when(redisTemplate).execute(any(), anyList(), anyString(), anyString()); 81 | 82 | this.request.setRequestURI("/serviceA"); 83 | this.request.setRemoteAddr("10.0.0.100"); 84 | this.request.setMethod("GET"); 85 | 86 | assertTrue(this.filter.shouldFilter()); 87 | 88 | String key = "-null_serviceA_10.0.0.100_anonymous_GET"; 89 | 90 | long requestCounter = 2; 91 | for (int i = 0; i < 2; i++) { 92 | this.filter.run(); 93 | Long remaining = Long.valueOf(Objects.requireNonNull(this.response.getHeader(HEADER_REMAINING + key))); 94 | assertEquals(--requestCounter, remaining); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/filters/pre/ConsulRateLimitPreFilterTest.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters.pre; 2 | 3 | import static com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.RateLimitConstants.HEADER_REMAINING; 4 | import static java.util.concurrent.TimeUnit.SECONDS; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | import static org.junit.jupiter.api.Assertions.assertTrue; 7 | import static org.mockito.ArgumentMatchers.anyString; 8 | import static org.mockito.Mockito.mock; 9 | import static org.mockito.Mockito.when; 10 | 11 | import com.ecwid.consul.v1.ConsulClient; 12 | import com.ecwid.consul.v1.Response; 13 | import com.ecwid.consul.v1.kv.model.GetValue; 14 | import com.fasterxml.jackson.databind.ObjectMapper; 15 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.Rate; 16 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.ConsulRateLimiter; 17 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.RateLimiterErrorHandler; 18 | import java.util.Date; 19 | import java.util.concurrent.TimeUnit; 20 | import org.junit.jupiter.api.BeforeEach; 21 | import org.junit.jupiter.api.Test; 22 | 23 | /** 24 | * @author Marcos Barbero 25 | * @since 2017-08-28 26 | */ 27 | public class ConsulRateLimitPreFilterTest extends BaseRateLimitPreFilterTest { 28 | 29 | private ConsulClient consulClient; 30 | private ObjectMapper objectMapper = new ObjectMapper(); 31 | 32 | private Rate rate(long remaining) { 33 | return new Rate("key", remaining, 2000L, 100L, new Date(System.currentTimeMillis() + SECONDS.toMillis(2))); 34 | } 35 | 36 | @BeforeEach 37 | @Override 38 | public void setUp() { 39 | RateLimiterErrorHandler rateLimiterErrorHandler = mock(RateLimiterErrorHandler.class); 40 | consulClient = mock(ConsulClient.class); 41 | this.setRateLimiter(new ConsulRateLimiter(rateLimiterErrorHandler, this.consulClient, this.objectMapper)); 42 | super.setUp(); 43 | } 44 | 45 | @Test 46 | @Override 47 | @SuppressWarnings("unchecked") 48 | public void testRateLimitExceedCapacity() throws Exception { 49 | Response response = mock(Response.class); 50 | GetValue getValue = mock(GetValue.class); 51 | when(this.consulClient.getKVValue(anyString())).thenReturn(response); 52 | when(response.getValue()).thenReturn(getValue); 53 | when(getValue.getDecodedValue()).thenReturn(this.objectMapper.writeValueAsString(this.rate(-1))); 54 | super.testRateLimitExceedCapacity(); 55 | } 56 | 57 | @Test 58 | @Override 59 | @SuppressWarnings("unchecked") 60 | public void testRateLimit() throws Exception { 61 | Response response = mock(Response.class); 62 | GetValue getValue = mock(GetValue.class); 63 | when(this.consulClient.getKVValue(anyString())).thenReturn(response); 64 | when(response.getValue()).thenReturn(getValue); 65 | when(getValue.getDecodedValue()).thenReturn(this.objectMapper.writeValueAsString(this.rate(1))); 66 | 67 | this.request.setRequestURI("/serviceA"); 68 | this.request.setRemoteAddr("10.0.0.100"); 69 | 70 | assertTrue(this.filter.shouldFilter()); 71 | 72 | for (int i = 0; i < 2; i++) { 73 | this.filter.run(); 74 | } 75 | 76 | String key = "-null_serviceA_10.0.0.100_anonymous"; 77 | String remaining = this.response.getHeader(HEADER_REMAINING + key); 78 | assertEquals("0", remaining); 79 | 80 | TimeUnit.SECONDS.sleep(2); 81 | 82 | when(getValue.getDecodedValue()).thenReturn(this.objectMapper.writeValueAsString(this.rate(2))); 83 | this.filter.run(); 84 | remaining = this.response.getHeader(HEADER_REMAINING + key); 85 | assertEquals("1", remaining); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/RedisRateLimiter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository; 18 | 19 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.Rate; 20 | import org.springframework.core.io.ClassPathResource; 21 | import org.springframework.data.redis.core.StringRedisTemplate; 22 | import org.springframework.data.redis.core.script.DefaultRedisScript; 23 | import org.springframework.data.redis.core.script.RedisScript; 24 | 25 | import java.time.Duration; 26 | import java.util.Arrays; 27 | import java.util.Collections; 28 | import java.util.Objects; 29 | 30 | import static java.util.concurrent.TimeUnit.SECONDS; 31 | 32 | /** 33 | * @author Marcos Barbero 34 | * @author Liel Chayoun 35 | */ 36 | public class RedisRateLimiter extends AbstractNonBlockCacheRateLimiter { 37 | 38 | private final RateLimiterErrorHandler rateLimiterErrorHandler; 39 | private final StringRedisTemplate redisTemplate; 40 | private final RedisScript redisScript; 41 | 42 | public RedisRateLimiter(final RateLimiterErrorHandler rateLimiterErrorHandler, 43 | final StringRedisTemplate redisTemplate) { 44 | this.rateLimiterErrorHandler = rateLimiterErrorHandler; 45 | this.redisTemplate = redisTemplate; 46 | this.redisScript = getScript(); 47 | } 48 | 49 | @Override 50 | protected void calcRemainingLimit(final Long limit, final Duration refreshInterval, 51 | final Long requestTime, final String key, final Rate rate) { 52 | if (Objects.nonNull(limit)) { 53 | long usage = requestTime == null ? 1L : 0L; 54 | Long remaining = calcRemaining(limit, refreshInterval, usage, key, rate); 55 | rate.setRemaining(remaining); 56 | } 57 | } 58 | 59 | @Override 60 | protected void calcRemainingQuota(final Long quota, final Duration refreshInterval, 61 | final Long requestTime, final String key, final Rate rate) { 62 | if (Objects.nonNull(quota)) { 63 | String quotaKey = key + QUOTA_SUFFIX; 64 | long usage = requestTime != null ? requestTime : 0L; 65 | Long remaining = calcRemaining(quota, refreshInterval, usage, quotaKey, rate); 66 | rate.setRemainingQuota(remaining); 67 | } 68 | } 69 | 70 | private Long calcRemaining(Long limit, Duration refreshInterval, long usage, String key, Rate rate) { 71 | rate.setReset(refreshInterval.toMillis()); 72 | Long current = 0L; 73 | try { 74 | current = redisTemplate.execute(redisScript, Collections.singletonList(key), Long.toString(usage), 75 | Long.toString(refreshInterval.getSeconds())); 76 | } catch (RuntimeException e) { 77 | String msg = "Failed retrieving rate for " + key + ", will return the current value"; 78 | rateLimiterErrorHandler.handleError(msg, e); 79 | } 80 | return Math.max(-1, limit - (current != null ? current.intValue() : 0)); 81 | } 82 | 83 | private RedisScript getScript() { 84 | DefaultRedisScript redisScript = new DefaultRedisScript<>(); 85 | redisScript.setLocation(new ClassPathResource("/scripts/ratelimit.lua")); 86 | redisScript.setResultType(Long.class); 87 | return redisScript; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/RedisRateLimiterTest.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository; 2 | 3 | import com.google.common.collect.Maps; 4 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Disabled; 7 | import org.junit.jupiter.api.Test; 8 | import org.mockito.Mock; 9 | import org.mockito.Mockito; 10 | import org.mockito.MockitoAnnotations; 11 | import org.springframework.data.redis.core.BoundValueOperations; 12 | import org.springframework.data.redis.core.StringRedisTemplate; 13 | import org.springframework.data.redis.core.ValueOperations; 14 | 15 | import java.time.Duration; 16 | import java.util.Map; 17 | 18 | import static org.mockito.ArgumentMatchers.*; 19 | import static org.mockito.Mockito.*; 20 | 21 | @SuppressWarnings("unchecked") 22 | public class RedisRateLimiterTest extends BaseRateLimiterTest { 23 | 24 | @Mock 25 | private RateLimiterErrorHandler rateLimiterErrorHandler; 26 | @Mock 27 | private StringRedisTemplate redisTemplate; 28 | 29 | @BeforeEach 30 | public void setUp() { 31 | MockitoAnnotations.initMocks(this); 32 | doReturn(1L, 2L) 33 | .when(redisTemplate).execute(any(), anyList(), anyString(), anyString()); 34 | 35 | this.target = new RedisRateLimiter(this.rateLimiterErrorHandler, this.redisTemplate); 36 | } 37 | 38 | @Test 39 | @Disabled 40 | public void testConsumeOnlyQuota() { 41 | // disabling in favor of integration tests 42 | } 43 | 44 | @Test 45 | @Disabled 46 | public void testConsume() { 47 | // disabling in favor of integration tests 48 | } 49 | 50 | @Test 51 | public void testConsumeRemainingLimitException() { 52 | doThrow(new RuntimeException()).when(redisTemplate).execute(any(), anyList(), anyString(), anyString()); 53 | 54 | Policy policy = new Policy(); 55 | policy.setLimit(100L); 56 | target.consume(policy, "key", 0L); 57 | verify(rateLimiterErrorHandler).handleError(matches(".* key, .*"), any()); 58 | } 59 | 60 | @Test 61 | public void testConsumeRemainingQuotaLimitException() { 62 | doThrow(new RuntimeException()).when(redisTemplate).execute(any(), anyList(), anyString(), anyString()); 63 | 64 | Policy policy = new Policy(); 65 | policy.setQuota(Duration.ofSeconds(100)); 66 | target.consume(policy, "key", 0L); 67 | verify(rateLimiterErrorHandler).handleError(matches(".* key-quota, .*"), any()); 68 | } 69 | 70 | @Test 71 | public void testConsumeGetExpireException() { 72 | doThrow(new RuntimeException()).when(redisTemplate).execute(any(), anyList(), anyString(), anyString()); 73 | 74 | Policy policy = new Policy(); 75 | policy.setLimit(100L); 76 | policy.setQuota(Duration.ofSeconds(50)); 77 | target.consume(policy, "key", 0L); 78 | verify(rateLimiterErrorHandler).handleError(matches(".* key, .*"), any()); 79 | verify(rateLimiterErrorHandler).handleError(matches(".* key-quota, .*"), any()); 80 | } 81 | 82 | @Test 83 | public void testConsumeExpireException() { 84 | doThrow(new RuntimeException()).when(redisTemplate).execute(any(), anyList(), anyString(), anyString()); 85 | 86 | Policy policy = new Policy(); 87 | policy.setLimit(100L); 88 | target.consume(policy, "key", 0L); 89 | verify(rateLimiterErrorHandler).handleError(matches(".* key, .*"), any()); 90 | } 91 | 92 | @Test 93 | public void testConsumeSetKey() { 94 | doReturn(1L, 2L) 95 | .when(redisTemplate).execute(any(), anyList(), anyString(), anyString()); 96 | 97 | Policy policy = new Policy(); 98 | policy.setLimit(20L); 99 | target.consume(policy, "key", 0L); 100 | 101 | verify(redisTemplate).execute(any(), anyList(), anyString(), anyString()); 102 | verify(rateLimiterErrorHandler, never()).handleError(any(), any()); 103 | } 104 | } -------------------------------------------------------------------------------- /spring-cloud-starter-zuul-ratelimit/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-cloud-zuul-ratelimit-parent 7 | com.marcosbarbero.cloud 8 | 2.4.3.RELEASE 9 | .. 10 | 11 | 4.0.0 12 | 13 | spring-cloud-zuul-ratelimit 14 | Spring Cloud Starter Zuul - Rate Limit 15 | Spring Cloud Starter 16 | 17 | 18 | 19 | com.marcosbarbero.cloud 20 | spring-cloud-zuul-ratelimit-core 21 | 22 | 23 | 24 | 25 | 26 | ossrh 27 | https://oss.sonatype.org/content/repositories/snapshots 28 | 29 | 30 | ossrh 31 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 32 | 33 | 34 | 35 | 36 | 37 | deploy 38 | 39 | 40 | 41 | org.apache.maven.plugins 42 | maven-gpg-plugin 43 | 3.0.1 44 | 45 | 46 | sign-artifacts 47 | verify 48 | 49 | sign 50 | 51 | 52 | 53 | 54 | 55 | 56 | org.sonatype.plugins 57 | nexus-staging-maven-plugin 58 | ${nexus-staging-maven-plugin.version} 59 | true 60 | 61 | ossrh 62 | https://oss.sonatype.org/ 63 | true 64 | 65 | 66 | 67 | 68 | org.apache.maven.plugins 69 | maven-source-plugin 70 | 71 | 72 | attach-sources 73 | 74 | jar 75 | 76 | 77 | 78 | 79 | 80 | 81 | org.apache.maven.plugins 82 | maven-javadoc-plugin 83 | 84 | 85 | attach-javadocs 86 | 87 | jar 88 | 89 | 90 | 91 | 92 | -Xdoclint:none 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-tests/security-context/src/main/java/com/marcosbarbero/tests/SecurityContextApplication.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.tests; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.netflix.zuul.EnableZuulProxy; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 11 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 12 | import org.springframework.security.core.userdetails.User; 13 | import org.springframework.security.core.userdetails.UserDetails; 14 | import org.springframework.security.core.userdetails.UserDetailsService; 15 | import org.springframework.security.provisioning.InMemoryUserDetailsManager; 16 | import org.springframework.web.bind.annotation.GetMapping; 17 | import org.springframework.web.bind.annotation.PathVariable; 18 | import org.springframework.web.bind.annotation.RestController; 19 | import redis.embedded.RedisServer; 20 | 21 | import javax.annotation.PostConstruct; 22 | import javax.annotation.PreDestroy; 23 | import java.io.IOException; 24 | import java.net.Socket; 25 | 26 | @EnableZuulProxy 27 | @SpringBootApplication 28 | public class SecurityContextApplication { 29 | 30 | public static void main(String... args) { 31 | SpringApplication.run(SecurityContextApplication.class, args); 32 | } 33 | 34 | @RestController 35 | public static class ServiceController { 36 | 37 | static final String RESPONSE_BODY = "ResponseBody"; 38 | 39 | @GetMapping("/serviceA") 40 | public ResponseEntity serviceA() { 41 | return ResponseEntity.ok(RESPONSE_BODY); 42 | } 43 | 44 | @GetMapping("/serviceB") 45 | public ResponseEntity serviceB() { 46 | return ResponseEntity.ok(RESPONSE_BODY); 47 | } 48 | 49 | @GetMapping("/serviceC") 50 | public ResponseEntity serviceC() { 51 | return ResponseEntity.ok(RESPONSE_BODY); 52 | } 53 | 54 | @GetMapping("/serviceD/{paramName}") 55 | public ResponseEntity serviceD(@PathVariable String paramName) { 56 | return ResponseEntity.ok(RESPONSE_BODY + " " + paramName); 57 | } 58 | 59 | @GetMapping("/serviceE") 60 | public ResponseEntity serviceE() throws InterruptedException { 61 | Thread.sleep(1100); 62 | return ResponseEntity.ok(RESPONSE_BODY); 63 | } 64 | } 65 | 66 | @Configuration 67 | public static class RedisConfig { 68 | 69 | private static final int DEFAULT_PORT = 6380; 70 | 71 | private RedisServer redisServer; 72 | 73 | private static boolean available(int port) { 74 | try (Socket ignored = new Socket("localhost", port)) { 75 | return false; 76 | } catch (IOException ignored) { 77 | return true; 78 | } 79 | } 80 | 81 | @PostConstruct 82 | public void setUp() throws IOException { 83 | this.redisServer = new RedisServer(DEFAULT_PORT); 84 | if (available(DEFAULT_PORT)) { 85 | this.redisServer.start(); 86 | } 87 | } 88 | 89 | @PreDestroy 90 | public void destroy() { 91 | this.redisServer.stop(); 92 | } 93 | } 94 | 95 | @Configuration 96 | @EnableWebSecurity 97 | static class SecurityConfig extends WebSecurityConfigurerAdapter { 98 | 99 | @Bean 100 | @Override 101 | @SuppressWarnings("deprecation") 102 | public UserDetailsService userDetailsService() { 103 | UserDetails user = 104 | User.withDefaultPasswordEncoder() 105 | .username("user") 106 | .password("user") 107 | .roles("USER") 108 | .build(); 109 | 110 | UserDetails admin = User.withDefaultPasswordEncoder() 111 | .username("admin") 112 | .password("admin") 113 | .roles("ADMIN") 114 | .build(); 115 | 116 | return new InMemoryUserDetailsManager(user, admin); 117 | } 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/main/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/config/repository/bucket4j/AbstractBucket4jRateLimiter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.bucket4j; 18 | 19 | import static java.util.concurrent.TimeUnit.NANOSECONDS; 20 | 21 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.Rate; 22 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.repository.AbstractCacheRateLimiter; 23 | import io.github.bucket4j.AbstractBucketBuilder; 24 | import io.github.bucket4j.Bandwidth; 25 | import io.github.bucket4j.Bucket; 26 | import io.github.bucket4j.Bucket4j; 27 | import io.github.bucket4j.BucketConfiguration; 28 | import io.github.bucket4j.ConsumptionProbe; 29 | import io.github.bucket4j.Extension; 30 | import io.github.bucket4j.grid.ProxyManager; 31 | import java.time.Duration; 32 | import java.util.function.Supplier; 33 | 34 | /** 35 | * Bucket4j rate limiter configuration. 36 | * 37 | * @author Liel Chayoun 38 | * @since 2018-04-06 39 | */ 40 | abstract class AbstractBucket4jRateLimiter, E extends Extension> extends AbstractCacheRateLimiter { 41 | 42 | private final Class extension; 43 | private ProxyManager buckets; 44 | 45 | AbstractBucket4jRateLimiter(final Class extension) { 46 | this.extension = extension; 47 | } 48 | 49 | void init() { 50 | buckets = getProxyManager(getExtension()); 51 | } 52 | 53 | private E getExtension() { 54 | return Bucket4j.extension(extension); 55 | } 56 | 57 | protected abstract ProxyManager getProxyManager(E extension); 58 | 59 | private Bucket getQuotaBucket(String key, Long quota, Duration refreshInterval) { 60 | return buckets.getProxy(key + QUOTA_SUFFIX, getBucketConfiguration(quota, refreshInterval)); 61 | } 62 | 63 | private Bucket getLimitBucket(String key, Long limit, Duration refreshInterval) { 64 | return buckets.getProxy(key, getBucketConfiguration(limit, refreshInterval)); 65 | } 66 | 67 | private Supplier getBucketConfiguration(Long capacity, Duration period) { 68 | return () -> Bucket4j.configurationBuilder().addLimit(Bandwidth.simple(capacity, period)).build(); 69 | } 70 | 71 | private void setRemaining(Rate rate, long remaining, boolean isQuota) { 72 | if (isQuota) { 73 | rate.setRemainingQuota(remaining); 74 | } else { 75 | rate.setRemaining(remaining); 76 | } 77 | } 78 | 79 | private void calcAndSetRemainingBucket(Long consume, Rate rate, Bucket bucket, boolean isQuota) { 80 | ConsumptionProbe consumptionProbe = bucket.tryConsumeAndReturnRemaining(consume); 81 | long nanosToWaitForRefill = consumptionProbe.getNanosToWaitForRefill(); 82 | rate.setReset(NANOSECONDS.toMillis(nanosToWaitForRefill)); 83 | if (consumptionProbe.isConsumed()) { 84 | long remainingTokens = consumptionProbe.getRemainingTokens(); 85 | setRemaining(rate, remainingTokens, isQuota); 86 | } else { 87 | setRemaining(rate, -1L, isQuota); 88 | bucket.tryConsumeAsMuchAsPossible(consume); 89 | } 90 | } 91 | 92 | private void calcAndSetRemainingBucket(Bucket bucket, Rate rate, boolean isQuota) { 93 | long availableTokens = bucket.getAvailableTokens(); 94 | long remaining = availableTokens > 0 ? availableTokens : -1; 95 | setRemaining(rate, remaining, isQuota); 96 | } 97 | 98 | @Override 99 | protected void calcRemainingLimit(final Long limit, final Duration refreshInterval, final Long requestTime, 100 | final String key, final Rate rate) { 101 | if (limit == null) { 102 | return; 103 | } 104 | Bucket bucket = getLimitBucket(key, limit, refreshInterval); 105 | if (requestTime == null) { 106 | calcAndSetRemainingBucket(1L, rate, bucket, false); 107 | } else { 108 | calcAndSetRemainingBucket(bucket, rate, false); 109 | } 110 | } 111 | 112 | @Override 113 | protected void calcRemainingQuota(final Long quota, final Duration refreshInterval, final Long requestTime, 114 | final String key, final Rate rate) { 115 | if (quota == null) { 116 | return; 117 | } 118 | Bucket bucket = getQuotaBucket(key, quota, refreshInterval); 119 | if (requestTime != null) { 120 | calcAndSetRemainingBucket(requestTime, rate, bucket, true); 121 | } else { 122 | calcAndSetRemainingBucket(bucket, rate, true); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-core/src/test/java/com/marcosbarbero/cloud/autoconfigure/zuul/ratelimit/filters/post/RateLimitPostFilterTest.java: -------------------------------------------------------------------------------- 1 | package com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters.post; 2 | 3 | import static com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.RateLimitConstants.REQUEST_START_TIME; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | import static org.mockito.ArgumentMatchers.any; 6 | import static org.mockito.ArgumentMatchers.anyLong; 7 | import static org.mockito.ArgumentMatchers.eq; 8 | import static org.mockito.Mockito.verify; 9 | import static org.mockito.Mockito.verifyNoInteractions; 10 | import static org.mockito.Mockito.when; 11 | 12 | import com.google.common.collect.Lists; 13 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimitKeyGenerator; 14 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimitUtils; 15 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimiter; 16 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties; 17 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties.Policy; 18 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.filters.RateLimitPostFilter; 19 | import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.DefaultRateLimitUtils; 20 | import com.netflix.zuul.context.RequestContext; 21 | import java.time.Duration; 22 | import javax.servlet.http.HttpServletRequest; 23 | import org.junit.jupiter.api.BeforeEach; 24 | import org.junit.jupiter.api.Test; 25 | import org.mockito.Mock; 26 | import org.mockito.MockitoAnnotations; 27 | import org.springframework.cloud.netflix.zuul.filters.RouteLocator; 28 | import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; 29 | import org.springframework.web.context.request.RequestAttributes; 30 | import org.springframework.web.context.request.RequestContextHolder; 31 | import org.springframework.web.util.UrlPathHelper; 32 | 33 | public class RateLimitPostFilterTest { 34 | 35 | private RateLimitPostFilter target; 36 | 37 | @Mock 38 | private RouteLocator routeLocator; 39 | @Mock 40 | private RateLimiter rateLimiter; 41 | @Mock 42 | private RateLimitKeyGenerator rateLimitKeyGenerator; 43 | @Mock 44 | private RequestAttributes requestAttributes; 45 | @Mock 46 | private HttpServletRequest httpServletRequest; 47 | 48 | private RateLimitProperties rateLimitProperties = new RateLimitProperties(); 49 | 50 | @BeforeEach 51 | public void setUp() { 52 | MockitoAnnotations.initMocks(this); 53 | when(httpServletRequest.getContextPath()).thenReturn("/servicea/test"); 54 | when(httpServletRequest.getRequestURI()).thenReturn("/servicea/test"); 55 | RequestContext requestContext = new RequestContext(); 56 | requestContext.setRequest(httpServletRequest); 57 | RequestContext.testSetCurrentContext(requestContext); 58 | RequestContextHolder.setRequestAttributes(requestAttributes); 59 | rateLimitProperties = new RateLimitProperties(); 60 | UrlPathHelper urlPathHelper = new UrlPathHelper(); 61 | RateLimitUtils rateLimitUtils = new DefaultRateLimitUtils(rateLimitProperties); 62 | target = new RateLimitPostFilter(rateLimitProperties, routeLocator, urlPathHelper, rateLimiter, rateLimitKeyGenerator, rateLimitUtils); 63 | } 64 | 65 | @Test 66 | public void testFilterType() { 67 | assertThat(target.filterType()).isEqualTo(FilterConstants.POST_TYPE); 68 | } 69 | 70 | @Test 71 | public void testFilterOrder() { 72 | assertThat(target.filterOrder()).isEqualTo(FilterConstants.SEND_RESPONSE_FILTER_ORDER - 10); 73 | } 74 | 75 | @Test 76 | public void testShouldFilterOnDisabledProperty() { 77 | assertThat(target.shouldFilter()).isEqualTo(false); 78 | } 79 | 80 | @Test 81 | public void testShouldFilterOnNoPolicy() { 82 | rateLimitProperties.setEnabled(true); 83 | 84 | assertThat(target.shouldFilter()).isEqualTo(false); 85 | } 86 | 87 | @Test 88 | public void testShouldFilterOnNullStartTime() { 89 | rateLimitProperties.setEnabled(true); 90 | Policy defaultPolicy = new Policy(); 91 | rateLimitProperties.getDefaultPolicyList().add(defaultPolicy); 92 | 93 | assertThat(target.shouldFilter()).isEqualTo(false); 94 | } 95 | 96 | @Test 97 | public void testShouldFilter() { 98 | rateLimitProperties.setEnabled(true); 99 | when(httpServletRequest.getAttribute(REQUEST_START_TIME)).thenReturn(System.currentTimeMillis()); 100 | Policy defaultPolicy = new Policy(); 101 | rateLimitProperties.setDefaultPolicyList(Lists.newArrayList(defaultPolicy)); 102 | 103 | assertThat(target.shouldFilter()).isEqualTo(true); 104 | } 105 | 106 | @Test 107 | public void testRunNoPolicy() { 108 | target.run(); 109 | verifyNoInteractions(rateLimiter); 110 | } 111 | 112 | @Test 113 | public void testRun() { 114 | rateLimitProperties.setEnabled(true); 115 | when(httpServletRequest.getAttribute(REQUEST_START_TIME)).thenReturn(System.currentTimeMillis()); 116 | Policy defaultPolicy = new Policy(); 117 | defaultPolicy.setQuota(Duration.ofSeconds(2)); 118 | rateLimitProperties.setDefaultPolicyList(Lists.newArrayList(defaultPolicy)); 119 | when(rateLimitKeyGenerator.key(any(), any(), any())).thenReturn("generatedKey"); 120 | 121 | target.run(); 122 | verify(rateLimiter).consume(eq(defaultPolicy), eq("generatedKey"), anyLong()); 123 | } 124 | } -------------------------------------------------------------------------------- /spring-cloud-zuul-ratelimit-dependencies/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 4.0.0 7 | 8 | com.marcosbarbero.cloud 9 | spring-cloud-zuul-ratelimit-dependencies 10 | 2.4.3.RELEASE 11 | pom 12 | https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit 13 | 14 | spring-cloud-zuul-ratelimit-dependencies 15 | Spring Cloud Zuul Rate Limit Dependencies 16 | 17 | 18 | 1.6.8 19 | 20 | 21 | 22 | 23 | Apache License, Version 2.0 24 | http://www.apache.org/licenses/LICENSE-2.0.txt 25 | repo 26 | 27 | 28 | 29 | 30 | 31 | marcosbarbero 32 | marcos.hgb@gmail.com 33 | Marcos Barbero 34 | CET 35 | 36 | 37 | 38 | 39 | scm:git:https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit.git 40 | 41 | scm:git:git@github.com:marcosbarbero/spring-cloud-starter-ratelimit.git 42 | 43 | https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit 44 | HEAD 45 | 46 | 47 | 48 | 49 | 50 | 51 | com.marcosbarbero.cloud 52 | spring-cloud-zuul-ratelimit-core 53 | ${project.version} 54 | 55 | 56 | 57 | com.marcosbarbero.cloud 58 | spring-cloud-zuul-ratelimit 59 | ${project.version} 60 | 61 | 62 | 63 | 64 | 65 | 66 | ossrh 67 | https://oss.sonatype.org/content/repositories/snapshots 68 | 69 | 70 | ossrh 71 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 72 | 73 | 74 | 75 | 76 | 77 | deploy 78 | 79 | 80 | 81 | org.apache.maven.plugins 82 | maven-gpg-plugin 83 | 3.0.1 84 | 85 | 86 | sign-artifacts 87 | verify 88 | 89 | sign 90 | 91 | 92 | 93 | 94 | 95 | 96 | org.sonatype.plugins 97 | nexus-staging-maven-plugin 98 | ${nexus-staging-maven-plugin.version} 99 | true 100 | 101 | ossrh 102 | https://oss.sonatype.org/ 103 | true 104 | 105 | 106 | 107 | 108 | org.apache.maven.plugins 109 | maven-source-plugin 110 | 111 | 112 | attach-sources 113 | 114 | jar 115 | 116 | 117 | 118 | 119 | 120 | 121 | org.apache.maven.plugins 122 | maven-javadoc-plugin 123 | 124 | 125 | attach-javadocs 126 | 127 | jar 128 | 129 | 130 | 131 | 132 | -Xdoclint:none 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | --------------------------------------------------------------------------------