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