Skip to content

Commit fc378be

Browse files
committed
add whitelisted source address support and changed net size configuration names
1 parent b5d8069 commit fc378be

File tree

10 files changed

+71
-22
lines changed

10 files changed

+71
-22
lines changed

captchaservice-backend/src/main/java/de/muenchen/captchaservice/configuration/captcha/CaptchaSite.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,23 @@ public record CaptchaSite(
1111
String secret,
1212
@Min(1) Integer maxVerifiesPerPayload,
1313
@NotNull List<DifficultyItem> difficultyMap,
14-
15-
@Min(0) @Max(32) Integer sourceAddressIpv4Cidr,
16-
@Min(0) @Max(128) Integer sourceAddressIpv6Cidr) {
14+
@Min(0) @Max(32) Integer sourceAddressIpv4NetSize,
15+
@Min(0) @Max(128) Integer sourceAddressIpv6NetSize,
16+
List<String> whitelistedSourceAddresses) {
1717
public CaptchaSite(
1818
final String siteKey,
1919
final String secret,
2020
final Integer maxVerifiesPerPayload,
2121
final List<DifficultyItem> difficultyMap,
22-
final Integer sourceAddressIpv4Cidr,
23-
final Integer sourceAddressIpv6Cidr) {
22+
final Integer sourceAddressIpv4NetSize,
23+
final Integer sourceAddressIpv6NetSize,
24+
final List<String> whitelistedSourceAddresses) {
2425
this.siteKey = siteKey;
2526
this.secret = secret;
2627
this.maxVerifiesPerPayload = maxVerifiesPerPayload != null ? maxVerifiesPerPayload : 1;
2728
this.difficultyMap = List.copyOf(difficultyMap);
28-
this.sourceAddressIpv4Cidr = sourceAddressIpv4Cidr != null ? sourceAddressIpv4Cidr : 32;
29-
this.sourceAddressIpv6Cidr = sourceAddressIpv6Cidr != null ? sourceAddressIpv6Cidr : 128;
29+
this.sourceAddressIpv4NetSize = sourceAddressIpv4NetSize != null ? sourceAddressIpv4NetSize : 32;
30+
this.sourceAddressIpv6NetSize = sourceAddressIpv6NetSize != null ? sourceAddressIpv6NetSize : 128;
31+
this.whitelistedSourceAddresses = whitelistedSourceAddresses != null ? List.copyOf(whitelistedSourceAddresses) : List.of();
3032
}
3133
}

captchaservice-backend/src/main/java/de/muenchen/captchaservice/data/SourceAddress.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package de.muenchen.captchaservice.data;
22

33
import lombok.AllArgsConstructor;
4+
import lombok.Data;
45
import org.apache.commons.codec.digest.DigestUtils;
56

67
@AllArgsConstructor
8+
@Data
79
public class SourceAddress {
810

911
final private String sourceAddress;

captchaservice-backend/src/main/java/de/muenchen/captchaservice/entity/CaptchaRequest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,15 @@ public class CaptchaRequest extends BaseEntity {
4343
@Size(min = 64, max = 64)
4444
private String sourceAddressHash;
4545

46+
@NotNull
47+
private boolean isWhitelisted;
48+
4649
@NotNull
4750
private Instant expiresAt;
4851

49-
public CaptchaRequest(String sourceAddressHash, Instant expiresAt) {
52+
public CaptchaRequest(String sourceAddressHash, boolean isWhitelisted, Instant expiresAt) {
5053
this.sourceAddressHash = sourceAddressHash;
54+
this.isWhitelisted = isWhitelisted;
5155
this.expiresAt = expiresAt;
5256
}
5357

captchaservice-backend/src/main/java/de/muenchen/captchaservice/service/captcha/CaptchaService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public CaptchaService(final CaptchaProperties captchaProperties, final Difficult
3232

3333
public Altcha.Challenge createChallenge(final String siteKey, final SourceAddress sourceAddress) {
3434
final long difficulty = difficultyService.getDifficultyForSourceAddress(siteKey, sourceAddress);
35-
difficultyService.registerRequest(sourceAddress);
35+
difficultyService.registerRequest(siteKey, sourceAddress);
3636
final Altcha.ChallengeOptions options = new Altcha.ChallengeOptions();
3737
options.algorithm = Altcha.Algorithm.SHA256;
3838
options.hmacKey = captchaProperties.hmacKey();

captchaservice-backend/src/main/java/de/muenchen/captchaservice/service/difficulty/DifficultyService.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import de.muenchen.captchaservice.repository.CaptchaRequestRepository;
99
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
1010
import lombok.extern.slf4j.Slf4j;
11+
import org.springframework.security.web.util.matcher.IpAddressMatcher;
1112
import org.springframework.stereotype.Service;
1213

1314
import java.time.Instant;
@@ -27,15 +28,18 @@ public DifficultyService(final CaptchaProperties captchaProperties, final Captch
2728
this.captchaRequestRepository = captchaRequestRepository;
2829
}
2930

30-
public void registerRequest(final SourceAddress sourceAddress) {
31+
public void registerRequest(final String siteKey, final SourceAddress sourceAddress) {
3132
final String sourceAddressHash = sourceAddress.getHash();
32-
final CaptchaRequest captchaRequest = new CaptchaRequest(sourceAddressHash,
33+
final CaptchaRequest captchaRequest = new CaptchaRequest(sourceAddressHash, isSourceAddressWhitelisted(siteKey, sourceAddress),
3334
Instant.now().plusSeconds(captchaProperties.sourceAddressWindowSeconds()));
3435
captchaRequestRepository.save(captchaRequest);
3536
log.debug("Registered request for source address with hash {}", sourceAddressHash);
3637
}
3738

3839
public long getDifficultyForSourceAddress(final String siteKey, final SourceAddress sourceAddress) {
40+
if (isSourceAddressWhitelisted(siteKey, sourceAddress)) {
41+
return 1L;
42+
}
3943
final CaptchaSite captchaSite = captchaProperties.sites().get(siteKey);
4044
if (captchaSite == null) {
4145
throw new IllegalArgumentException("siteKey not found");
@@ -58,4 +62,12 @@ public long getDifficultyForSourceAddress(final String siteKey, final SourceAddr
5862
return maxNumber;
5963
}
6064

65+
public boolean isSourceAddressWhitelisted(final String siteKey, final SourceAddress sourceAddress) {
66+
final CaptchaSite captchaSite = captchaProperties.sites().get(siteKey);
67+
for (String subnet : captchaSite.whitelistedSourceAddresses()) {
68+
if (new IpAddressMatcher(subnet).matches(sourceAddress.getSourceAddress())) return true;
69+
}
70+
return false;
71+
}
72+
6173
}

captchaservice-backend/src/main/java/de/muenchen/captchaservice/service/sourceaddress/SourceAddressService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ public SourceAddress parse(final String siteKey, final String sourceAddress) {
2020
String networkAddressString;
2121
InetAddress addr = InetAddresses.forString(sourceAddress);
2222
if (addr instanceof java.net.Inet4Address) {
23-
networkAddressString = NetworkAddressCalculator.getNetworkAddress(sourceAddress, site.sourceAddressIpv4Cidr());
23+
networkAddressString = NetworkAddressCalculator.getNetworkAddress(sourceAddress, site.sourceAddressIpv4NetSize());
2424
} else if (addr instanceof java.net.Inet6Address) {
25-
networkAddressString = NetworkAddressCalculator.getNetworkAddress(sourceAddress, site.sourceAddressIpv6Cidr());
25+
networkAddressString = NetworkAddressCalculator.getNetworkAddress(sourceAddress, site.sourceAddressIpv6NetSize());
2626
} else {
2727
throw new IllegalArgumentException("Unsupported IP address type: " + addr.getClass().getName());
2828
}

captchaservice-backend/src/main/resources/db/migration/schema/V001__Init.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ CREATE TABLE captcha_request
33
id UUID NOT NULL,
44
request_at TIMESTAMP WITH TIME ZONE,
55
source_address_hash VARCHAR(64) NOT NULL,
6+
is_whitelisted BOOLEAN NOT NULL,
67
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
78
CONSTRAINT pk_captcharequest PRIMARY KEY (id)
89
);

captchaservice-backend/src/test/java/de/muenchen/captchaservice/service/difficulty/DifficultyServiceTest.java

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,49 @@ void testDifficultyIncrease() {
4343
final SourceAddress sourceAddress = new SourceAddress("1.2.3.4");
4444
long difficulty;
4545
// --
46-
difficultyService.registerRequest(sourceAddress);
46+
difficultyService.registerRequest("test_site", sourceAddress);
4747
difficulty = difficultyService.getDifficultyForSourceAddress(TEST_SITE_KEY, sourceAddress);
4848
assertEquals(1_000L, difficulty);
4949
// --
50-
difficultyService.registerRequest(sourceAddress);
50+
difficultyService.registerRequest("test_site", sourceAddress);
5151
difficulty = difficultyService.getDifficultyForSourceAddress(TEST_SITE_KEY, sourceAddress);
5252
assertEquals(2_000L, difficulty);
5353
// --
54-
difficultyService.registerRequest(sourceAddress);
54+
difficultyService.registerRequest("test_site", sourceAddress);
5555
difficulty = difficultyService.getDifficultyForSourceAddress(TEST_SITE_KEY, sourceAddress);
5656
assertEquals(3_000L, difficulty);
5757
// --
5858
for (int i = 0; i < 5; i++) {
59-
difficultyService.registerRequest(sourceAddress);
59+
difficultyService.registerRequest("test_site", sourceAddress);
6060
}
6161
difficulty = difficultyService.getDifficultyForSourceAddress(TEST_SITE_KEY, sourceAddress);
6262
assertEquals(3_000L, difficulty);
6363
}
6464

65+
@Test
66+
@SneakyThrows
67+
void testDifficultyIncreaseWithWhitelistedSourceAddress() {
68+
databaseTestUtil.clearDatabase();
69+
final SourceAddress sourceAddress = new SourceAddress("10.1.2.3");
70+
long difficulty;
71+
// --
72+
difficultyService.registerRequest("test_site", sourceAddress);
73+
difficulty = difficultyService.getDifficultyForSourceAddress(TEST_SITE_KEY, sourceAddress);
74+
assertEquals(1L, difficulty);
75+
// --
76+
difficultyService.registerRequest("test_site", sourceAddress);
77+
difficulty = difficultyService.getDifficultyForSourceAddress(TEST_SITE_KEY, sourceAddress);
78+
assertEquals(1L, difficulty);
79+
// --
80+
difficultyService.registerRequest("test_site", sourceAddress);
81+
difficulty = difficultyService.getDifficultyForSourceAddress(TEST_SITE_KEY, sourceAddress);
82+
assertEquals(1L, difficulty);
83+
// --
84+
for (int i = 0; i < 5; i++) {
85+
difficultyService.registerRequest("test_site", sourceAddress);
86+
}
87+
difficulty = difficultyService.getDifficultyForSourceAddress(TEST_SITE_KEY, sourceAddress);
88+
assertEquals(1L, difficulty);
89+
}
90+
6591
}

captchaservice-backend/src/test/java/de/muenchen/captchaservice/service/expireddata/ExpiredDataServiceTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ void deleteExpiredData() {
4949
final String expiredHash = DigestUtils.sha256Hex("expired");
5050

5151
// Not expired data
52-
captchaRequestRepository.save(new CaptchaRequest(notExpiredHash, Instant.now().plus(Period.ofDays(1))));
53-
captchaRequestRepository.save(new CaptchaRequest(notExpiredHash, Instant.now().plus(Period.ofDays(1))));
52+
captchaRequestRepository.save(new CaptchaRequest(notExpiredHash, false, Instant.now().plus(Period.ofDays(1))));
53+
captchaRequestRepository.save(new CaptchaRequest(notExpiredHash, false, Instant.now().plus(Period.ofDays(1))));
5454
invalidatedPayloadRepository.save(new InvalidatedPayload(notExpiredHash, Instant.now().plus(Period.ofDays(1))));
5555
invalidatedPayloadRepository.save(new InvalidatedPayload(notExpiredHash, Instant.now().plus(Period.ofDays(1))));
5656

5757
// Expired data
58-
captchaRequestRepository.save(new CaptchaRequest(expiredHash, Instant.now().minus(Period.ofDays(1))));
59-
captchaRequestRepository.save(new CaptchaRequest(expiredHash, Instant.now().minus(Period.ofDays(1))));
58+
captchaRequestRepository.save(new CaptchaRequest(expiredHash, false, Instant.now().minus(Period.ofDays(1))));
59+
captchaRequestRepository.save(new CaptchaRequest(expiredHash, false, Instant.now().minus(Period.ofDays(1))));
6060
invalidatedPayloadRepository.save(new InvalidatedPayload(expiredHash, Instant.now().minus(Period.ofDays(1))));
6161
invalidatedPayloadRepository.save(new InvalidatedPayload(expiredHash, Instant.now().minus(Period.ofDays(1))));
6262

captchaservice-backend/src/test/resources/application-test.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ captcha:
2424
sites:
2525
test_site:
2626
secret: test_secret
27-
source-address-ipv6-cidr: 64
27+
source-address-ipv6-net-size: 64
28+
whitelisted-source-addresses:
29+
- "10.0.0.0/8"
2830
difficulty-map:
2931
- minVisits: 1
3032
maxNumber: 1000

0 commit comments

Comments
 (0)