Skip to content

Commit 3d78333

Browse files
authored
Merge pull request #80 from it-at-m/feature/save-requests-with-netmask
Implemented saving captcha requests source addesses filtered by netmask
2 parents 6625b92 + 0a4a18e commit 3d78333

File tree

14 files changed

+333
-23
lines changed

14 files changed

+333
-23
lines changed

captchaservice-backend/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,16 @@
313313
<artifactId>altcha</artifactId>
314314
<version>1.1.2</version>
315315
</dependency>
316+
<dependency>
317+
<groupId>commons-net</groupId>
318+
<artifactId>commons-net</artifactId>
319+
<version>3.11.1</version>
320+
</dependency>
321+
<dependency>
322+
<groupId>com.google.guava</groupId>
323+
<artifactId>guava</artifactId>
324+
<version>33.4.8-jre</version>
325+
</dependency>
316326
</dependencies>
317327

318328
<scm>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package de.muenchen.captchaservice.common;
2+
3+
import org.springframework.http.HttpStatus;
4+
import org.springframework.web.server.ResponseStatusException;
5+
6+
public class InvalidSourceAddressException extends ResponseStatusException {
7+
public InvalidSourceAddressException(final String sourceAddress) {
8+
super(HttpStatus.BAD_REQUEST, "Source address " + sourceAddress + " is not valid.");
9+
}
10+
}
Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,31 @@
11
package de.muenchen.captchaservice.configuration.captcha;
22

3+
import jakarta.validation.constraints.Max;
34
import jakarta.validation.constraints.Min;
45
import jakarta.validation.constraints.NotNull;
56

67
import java.util.List;
78

8-
public record CaptchaSite(String siteKey, String secret, @Min(1) Integer maxVerifiesPerPayload, @NotNull List<DifficultyItem> difficultyMap) {
9-
public CaptchaSite(final String siteKey, final String secret, final Integer maxVerifiesPerPayload, final List<DifficultyItem> difficultyMap) {
9+
public record CaptchaSite(
10+
String siteKey,
11+
String secret,
12+
@Min(1) Integer maxVerifiesPerPayload,
13+
@NotNull List<DifficultyItem> difficultyMap,
14+
15+
@Min(0) @Max(32) Integer sourceAddressIpv4Cidr,
16+
@Min(0) @Max(128) Integer sourceAddressIpv6Cidr) {
17+
public CaptchaSite(
18+
final String siteKey,
19+
final String secret,
20+
final Integer maxVerifiesPerPayload,
21+
final List<DifficultyItem> difficultyMap,
22+
final Integer sourceAddressIpv4Cidr,
23+
final Integer sourceAddressIpv6Cidr) {
1024
this.siteKey = siteKey;
1125
this.secret = secret;
1226
this.maxVerifiesPerPayload = maxVerifiesPerPayload != null ? maxVerifiesPerPayload : 1;
1327
this.difficultyMap = List.copyOf(difficultyMap);
28+
this.sourceAddressIpv4Cidr = sourceAddressIpv4Cidr != null ? sourceAddressIpv4Cidr : 32;
29+
this.sourceAddressIpv6Cidr = sourceAddressIpv6Cidr != null ? sourceAddressIpv6Cidr : 128;
1430
}
1531
}

captchaservice-backend/src/main/java/de/muenchen/captchaservice/controller/captcha/CaptchaController.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@
66
import de.muenchen.captchaservice.controller.captcha.response.PostChallengeResponse;
77
import de.muenchen.captchaservice.controller.captcha.response.PostVerifyResponse;
88
import de.muenchen.captchaservice.data.SourceAddress;
9+
import de.muenchen.captchaservice.service.captcha.CaptchaService;
910
import de.muenchen.captchaservice.service.siteauth.SiteAuthService;
11+
import de.muenchen.captchaservice.service.sourceaddress.SourceAddressService;
1012
import jakarta.validation.Valid;
1113
import lombok.RequiredArgsConstructor;
1214
import lombok.SneakyThrows;
1315
import org.altcha.altcha.Altcha;
14-
import org.springframework.web.bind.annotation.*;
15-
import de.muenchen.captchaservice.service.captcha.CaptchaService;
16+
import org.springframework.web.bind.annotation.PostMapping;
17+
import org.springframework.web.bind.annotation.RequestBody;
18+
import org.springframework.web.bind.annotation.RequestMapping;
19+
import org.springframework.web.bind.annotation.RestController;
1620

1721
@RestController
1822
@RequestMapping("/api/v1/captcha")
@@ -21,6 +25,7 @@ public class CaptchaController {
2125

2226
private final CaptchaService captchaService;
2327
private final SiteAuthService siteAuthService;
28+
private final SourceAddressService sourceAddressService;
2429

2530
@PostMapping("/challenge")
2631
@SneakyThrows
@@ -29,7 +34,7 @@ public PostChallengeResponse postChallenge(@Valid @RequestBody final PostChallen
2934
throw new UnauthorizedException("Wrong credentials.");
3035
}
3136

32-
final SourceAddress sourceAddress = SourceAddress.parse(request.getClientAddress());
37+
final SourceAddress sourceAddress = sourceAddressService.parse(request.getSiteKey(), request.getClientAddress());
3338
final Altcha.Challenge challenge = captchaService.createChallenge(request.getSiteKey(), sourceAddress);
3439
return new PostChallengeResponse(challenge);
3540
}

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

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,12 @@
33
import lombok.AllArgsConstructor;
44
import org.apache.commons.codec.digest.DigestUtils;
55

6-
import java.net.InetAddress;
7-
import java.net.UnknownHostException;
8-
96
@AllArgsConstructor
107
public class SourceAddress {
118

12-
final private InetAddress inetAddress;
13-
14-
public static SourceAddress parse(final String sourceAddress) throws UnknownHostException {
15-
return new SourceAddress(InetAddress.getByName(sourceAddress));
16-
}
9+
final private String sourceAddress;
1710

1811
public String getHash() {
19-
return DigestUtils.sha256Hex(inetAddress.toString());
12+
return DigestUtils.sha256Hex(sourceAddress);
2013
}
2114
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package de.muenchen.captchaservice.service.sourceaddress;
2+
3+
import com.google.common.net.InetAddresses;
4+
import de.muenchen.captchaservice.configuration.captcha.CaptchaProperties;
5+
import de.muenchen.captchaservice.configuration.captcha.CaptchaSite;
6+
import de.muenchen.captchaservice.data.SourceAddress;
7+
import de.muenchen.captchaservice.util.networkaddresscalculator.NetworkAddressCalculator;
8+
import lombok.AllArgsConstructor;
9+
import org.springframework.stereotype.Service;
10+
11+
import java.net.InetAddress;
12+
13+
@Service
14+
@AllArgsConstructor
15+
public class SourceAddressService {
16+
private final CaptchaProperties captchaProperties;
17+
18+
public SourceAddress parse(final String siteKey, final String sourceAddress) {
19+
CaptchaSite site = captchaProperties.sites().get(siteKey);
20+
String networkAddressString;
21+
InetAddress addr = InetAddresses.forString(sourceAddress);
22+
if (addr instanceof java.net.Inet4Address) {
23+
networkAddressString = NetworkAddressCalculator.getNetworkAddress(sourceAddress, site.sourceAddressIpv4Cidr());
24+
} else if (addr instanceof java.net.Inet6Address) {
25+
networkAddressString = NetworkAddressCalculator.getNetworkAddress(sourceAddress, site.sourceAddressIpv6Cidr());
26+
} else {
27+
throw new IllegalArgumentException("Unsupported IP address type: " + addr.getClass().getName());
28+
}
29+
return new SourceAddress(networkAddressString);
30+
}
31+
32+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package de.muenchen.captchaservice.util.networkaddresscalculator;
2+
3+
import lombok.Getter;
4+
5+
public class InvalidAddressException extends RuntimeException {
6+
@Getter
7+
private final String address;
8+
9+
public InvalidAddressException(String message, String address, Exception cause) {
10+
super(message, cause);
11+
this.address = address;
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package de.muenchen.captchaservice.util.networkaddresscalculator;
2+
3+
import lombok.Getter;
4+
5+
public class InvalidNetSizeException extends RuntimeException {
6+
@Getter
7+
private final int netSize;
8+
9+
public InvalidNetSizeException(String message, int netSize) {
10+
super(message);
11+
this.netSize = netSize;
12+
}
13+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package de.muenchen.captchaservice.util.networkaddresscalculator;
2+
3+
import com.google.common.net.InetAddresses;
4+
5+
import java.net.InetAddress;
6+
import java.net.UnknownHostException;
7+
8+
public class NetworkAddressCalculator {
9+
10+
/**
11+
* Calculates the network address for a given IP address and netmask size (CIDR prefix).
12+
* Supports both IPv4 and IPv6 addresses.
13+
*
14+
* @param address The IP address string (e.g., "192.168.1.1", "2001:db8::1").
15+
* @param netSize The CIDR netmask size (e.g., 24 for IPv4, 64 for IPv6).
16+
* @return The network address string (e.g., "192.168.1.0", "2001:db8::").
17+
* @throws InvalidAddressException If the IP address is invalid.
18+
* @throws InvalidNetSizeException If the `netSize` is out of range for the given IP version.
19+
*/
20+
public static String getNetworkAddress(String address, int netSize) {
21+
try {
22+
InetAddress inetAddress = InetAddresses.forString(address);
23+
byte[] addressBytes = inetAddress.getAddress();
24+
int addressLengthBits = addressBytes.length * 8;
25+
26+
if (netSize < 0 || netSize > addressLengthBits) {
27+
throw new InvalidNetSizeException(
28+
"Net size " + netSize + " is out of range for a " +
29+
(addressLengthBits == 32 ? "IPv4" : "IPv6") + " address (0-" + addressLengthBits + ")",
30+
netSize);
31+
}
32+
33+
byte[] netAddressBytes = new byte[addressBytes.length];
34+
35+
// Apply the netmask bit by bit
36+
for (int i = 0; i < addressBytes.length; i++) {
37+
int byteNetSizeRemaining = netSize - (i * 8);
38+
if (byteNetSizeRemaining >= 8) {
39+
netAddressBytes[i] = addressBytes[i];
40+
} else if (byteNetSizeRemaining > 0) {
41+
int mask = 0xFF << (8 - byteNetSizeRemaining);
42+
netAddressBytes[i] = (byte) (addressBytes[i] & mask);
43+
} else {
44+
netAddressBytes[i] = 0;
45+
}
46+
}
47+
48+
InetAddress netInetAddress = InetAddress.getByAddress(netAddressBytes);
49+
return netInetAddress.getHostAddress();
50+
} catch (IllegalArgumentException | UnknownHostException e) {
51+
throw new InvalidAddressException("Invalid address: " + address, address, e);
52+
}
53+
}
54+
}

captchaservice-backend/src/main/java/de/muenchen/captchaservice/validation/SourceAddressConstraintValidator.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
package de.muenchen.captchaservice.validation;
22

3+
import com.google.common.net.InetAddresses;
34
import jakarta.validation.ConstraintValidator;
45
import jakarta.validation.ConstraintValidatorContext;
56

6-
import java.net.InetAddress;
7-
import java.net.UnknownHostException;
8-
97
public class SourceAddressConstraintValidator implements ConstraintValidator<ValidSourceAddress, String> {
108
@Override
119
public boolean isValid(final String value, final ConstraintValidatorContext constraintValidatorContext) {
1210
try {
13-
InetAddress.getByName(value);
14-
} catch (UnknownHostException e) {
11+
InetAddresses.forString(value);
12+
} catch (IllegalArgumentException e) {
1513
return false;
1614
}
1715
return true;

0 commit comments

Comments
 (0)