mirror of
https://github.com/spring-projects/spring-security.git
synced 2026-03-30 14:08:11 +00:00
Add InetAddressMatcher
Co-authored-by: Gábor Vaspöri <gabor.vaspori@gmail.com> Co-authored-by: Kian Jamali <kianjamali123@gmail.com> Co-authored-by: Rossen Stoyanchev <rstoyanchev@users.noreply.github.com>
This commit is contained in:
parent
d4589c0fcb
commit
cc6a005aa5
@ -1,4 +1,4 @@
|
||||
[[new]]
|
||||
= What's New in Spring Security 7.1
|
||||
|
||||
This is a placeholder for updates to Spring Security 7.1
|
||||
* https://github.com/spring-projects/spring-security/pull/18634[gh-18634] - Added javadoc:org.springframework.security.web.util.matcher.InetAddressMatcher[]
|
||||
|
||||
@ -16,9 +16,12 @@
|
||||
|
||||
package org.springframework.security.web.server.util.matcher;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.security.web.util.matcher.IpAddressMatcher;
|
||||
import org.springframework.security.web.util.matcher.InetAddressMatcher;
|
||||
import org.springframework.security.web.util.matcher.InetAddressMatchers;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
@ -31,7 +34,7 @@ import org.springframework.web.server.ServerWebExchange;
|
||||
*/
|
||||
public final class IpAddressServerWebExchangeMatcher implements ServerWebExchangeMatcher {
|
||||
|
||||
private final IpAddressMatcher ipAddressMatcher;
|
||||
private final InetAddressMatcher ipAddressMatcher;
|
||||
|
||||
/**
|
||||
* Takes a specific IP address or a range specified using the IP/Netmask (e.g.
|
||||
@ -41,7 +44,7 @@ public final class IpAddressServerWebExchangeMatcher implements ServerWebExchang
|
||||
*/
|
||||
public IpAddressServerWebExchangeMatcher(String ipAddress) {
|
||||
Assert.hasText(ipAddress, "IP address cannot be empty");
|
||||
this.ipAddressMatcher = new IpAddressMatcher(ipAddress);
|
||||
this.ipAddressMatcher = InetAddressMatchers.builder().includeAddresses(List.of(ipAddress)).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.web.util.matcher;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Matches an {@link InetAddress}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Rob Winch
|
||||
* @since 7.1
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface InetAddressMatcher {
|
||||
|
||||
/**
|
||||
* Whether the given address matches.
|
||||
* @param address the {@link InetAddress} to check (may be {@code null})
|
||||
* @return {@code true} if the address matches, {@code false} otherwise
|
||||
*/
|
||||
boolean matches(@Nullable InetAddress address);
|
||||
|
||||
/**
|
||||
* Whether the given address string matches.
|
||||
* @param address the IP address string to check (may be {@code null})
|
||||
* @return {@code true} if the address matches, {@code false} otherwise
|
||||
*/
|
||||
default boolean matches(@Nullable String address) {
|
||||
return (address != null) ? matches(InetAddressParser.parseAddress(address)) : false;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,379 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.web.util.matcher;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Factory for creating {@link InetAddressMatcher} instances with various matching
|
||||
* strategies for IP addresses.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @since 7.1
|
||||
*/
|
||||
public final class InetAddressMatchers {
|
||||
|
||||
private InetAddressMatchers() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new builder for configuring an {@link InetAddressMatcher}.
|
||||
* @return a new {@link Builder} instance
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new builder configured to match external (non-private) IP addresses.
|
||||
* @return a {@link Builder} configured to match external addresses
|
||||
*/
|
||||
public static Builder matchExternal() {
|
||||
return builder().matchAll(ExternalInetAddressMatcher.getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new builder configured to match internal (private) IP addresses.
|
||||
* <p>
|
||||
* Internal addresses include loopback addresses (127.0.0.0/8 for IPv4, ::1 for IPv6),
|
||||
* private IPv4 address ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), and IPv6
|
||||
* Unique Local Addresses (fc00::/7).
|
||||
* @return a {@link Builder} configured to match internal addresses
|
||||
*/
|
||||
public static Builder matchInternal() {
|
||||
return builder().matchAll(InternalInetAddressMatcher.getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for constructing {@link InetAddressMatcher} instances with various
|
||||
* matching rules.
|
||||
*
|
||||
* @author Kian Jamali
|
||||
* @author Gábor Vaspöri
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Rob Winch
|
||||
*/
|
||||
public static final class Builder {
|
||||
|
||||
private final List<InetAddressMatcher> matchers = new ArrayList<>();
|
||||
|
||||
private boolean reportOnly;
|
||||
|
||||
/**
|
||||
* Adds an include list matcher that permits only the specified addresses.
|
||||
* @param addresses the list of IP address patterns to include (cannot be null or
|
||||
* empty)
|
||||
* @return this builder for method chaining
|
||||
* @throws IllegalArgumentException if addresses is null or empty
|
||||
*/
|
||||
public Builder includeAddresses(List<String> addresses) {
|
||||
Assert.notEmpty(addresses, "addresses cannot be empty");
|
||||
List<InetAddressMatcher> matchers = addresses.stream()
|
||||
.<InetAddressMatcher>map(IpInetAddressMatcher::new)
|
||||
.toList();
|
||||
this.matchers.add(new IncludeListInetAddressMatcher(matchers));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an exclude list matcher that blocks the specified addresses.
|
||||
* @param addresses the list of IP address patterns to exclude (cannot be null or
|
||||
* empty)
|
||||
* @return this builder for method chaining
|
||||
* @throws IllegalArgumentException if addresses is null or empty
|
||||
*/
|
||||
public Builder excludeAddresses(List<String> addresses) {
|
||||
Assert.notEmpty(addresses, "addresses cannot be empty");
|
||||
List<InetAddressMatcher> matchers = addresses.stream()
|
||||
.<InetAddressMatcher>map(IpInetAddressMatcher::new)
|
||||
.toList();
|
||||
this.matchers.add(new ExcludeListInetAddressMatcher(matchers));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds custom matchers to the matcher chain. All matchers must match for an
|
||||
* address to be permitted.
|
||||
* @param matchers the custom {@link InetAddressMatcher} instances to add (cannot
|
||||
* be null or empty)
|
||||
* @return this builder for method chaining
|
||||
* @throws IllegalArgumentException if matchers is null or empty
|
||||
*/
|
||||
public Builder matchAll(InetAddressMatcher... matchers) {
|
||||
Assert.notEmpty(matchers, "matchers cannot be empty");
|
||||
for (InetAddressMatcher matcher : matchers) {
|
||||
this.matchers.add(matcher);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the matcher to operate in report-only mode. In this mode, matching
|
||||
* logic is evaluated and logged, but all addresses are allowed regardless of
|
||||
* match results.
|
||||
* @return this builder for method chaining
|
||||
*/
|
||||
public Builder reportOnly() {
|
||||
this.reportOnly = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the configured {@link InetAddressMatcher}.
|
||||
* @return the constructed {@link InetAddressMatcher}
|
||||
*/
|
||||
public InetAddressMatcher build() {
|
||||
return new CompositeInetAddressMatcher(this.matchers, this.reportOnly);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link InetAddressMatcher} that matches addresses against an include list. Only
|
||||
* addresses that match an entry in the include list are permitted.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Rob Winch
|
||||
*/
|
||||
static final class IncludeListInetAddressMatcher implements InetAddressMatcher {
|
||||
|
||||
private final List<InetAddressMatcher> includeList;
|
||||
|
||||
IncludeListInetAddressMatcher(List<InetAddressMatcher> includeList) {
|
||||
Assert.notEmpty(includeList, "includeList cannot be null or empty");
|
||||
this.includeList = new ArrayList<>(includeList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(@Nullable InetAddress address) {
|
||||
for (InetAddressMatcher matcher : this.includeList) {
|
||||
if (matcher.matches(address)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "IncludeListInetAddressMatcher[\"" + this.includeList + "\"]";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link InetAddressMatcher} that matches addresses against an exclude list.
|
||||
* Addresses that match an entry in the exclude list are rejected.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Rob Winch
|
||||
*/
|
||||
static final class ExcludeListInetAddressMatcher implements InetAddressMatcher {
|
||||
|
||||
private final List<InetAddressMatcher> excludeList;
|
||||
|
||||
ExcludeListInetAddressMatcher(List<InetAddressMatcher> excludeList) {
|
||||
Assert.notEmpty(excludeList, "excludeList cannot be null or empty");
|
||||
this.excludeList = new ArrayList<>(excludeList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(@Nullable InetAddress address) {
|
||||
for (InetAddressMatcher matcher : this.excludeList) {
|
||||
if (matcher.matches(address)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ExcludeListInetAddressMatcher[\"" + this.excludeList + "\"]";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link InetAddressMatcher} that matches internal (private) addresses.
|
||||
* <p>
|
||||
* Internal addresses include loopback addresses (127.0.0.0/8 for IPv4, ::1 for IPv6),
|
||||
* private IPv4 address ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), and IPv6
|
||||
* Unique Local Addresses (fc00::/7).
|
||||
*
|
||||
* @author Gábor Vaspöri
|
||||
* @author Kian Jamali
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Rob Winch
|
||||
*/
|
||||
static final class InternalInetAddressMatcher implements InetAddressMatcher {
|
||||
|
||||
private static final InternalInetAddressMatcher INSTANCE = new InternalInetAddressMatcher();
|
||||
|
||||
static InternalInetAddressMatcher getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private InternalInetAddressMatcher() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(@Nullable InetAddress address) {
|
||||
if (address == null) {
|
||||
return false;
|
||||
}
|
||||
if (address.isLoopbackAddress() || address.isLinkLocalAddress() || address.isSiteLocalAddress()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
byte[] rawAddress = address.getAddress();
|
||||
|
||||
if (rawAddress.length == 16) {
|
||||
// Convert signed bytes to unsigned ints for easier matching logic
|
||||
int[] iAddr = new int[rawAddress.length];
|
||||
for (int i = 0; i < rawAddress.length; i++) {
|
||||
iAddr[i] = Byte.toUnsignedInt(rawAddress[i]);
|
||||
}
|
||||
|
||||
/*
|
||||
* IPv6, check for Unique Local Addresses. We cannot rely on
|
||||
* Inet6Address.isSiteLocalAddress() here because the JVM implementation
|
||||
* dictates that fec0::/10 is the only site-local IPv6 address space,
|
||||
* based on the outdated RFC 2373. That RFC was deprecated by the IETF in
|
||||
* 2004 in favor of fc00::/7 (RFC 4193). To keep our private network
|
||||
* checking accurate to modern subnets, we maintain manual parsing.
|
||||
*/
|
||||
if (iAddr[0] == 0xfc || iAddr[0] == 0xfd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// IPv4/IPv6 translation, 64:ff9b
|
||||
if (iAddr[0] == 0x00 && iAddr[1] == 0x64 && iAddr[2] == 0xff && iAddr[3] == 0x9b) {
|
||||
try {
|
||||
InetAddress ipv4Part = InetAddress.getByAddress(
|
||||
new byte[] { rawAddress[12], rawAddress[13], rawAddress[14], rawAddress[15] });
|
||||
|
||||
if (ipv4Part.isLoopbackAddress() || ipv4Part.isLinkLocalAddress()
|
||||
|| ipv4Part.isSiteLocalAddress()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (java.net.UnknownHostException ex) {
|
||||
// Should not happen for 4-byte array
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "InternalInetAddressMatcher";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An {@link InetAddressMatcher} that matches external (public) addresses.
|
||||
* <p>
|
||||
* External addresses are any addresses that are not internal (private) addresses.
|
||||
* This matcher delegates to {@link InternalInetAddressMatcher} and negates the
|
||||
* result.
|
||||
*
|
||||
* @author Gábor Vaspöri
|
||||
* @author Kian Jamali
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Rob Winch
|
||||
*/
|
||||
static final class ExternalInetAddressMatcher implements InetAddressMatcher {
|
||||
|
||||
private static final ExternalInetAddressMatcher INSTANCE = new ExternalInetAddressMatcher();
|
||||
|
||||
static ExternalInetAddressMatcher getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private final InternalInetAddressMatcher internalMatcher = InternalInetAddressMatcher.getInstance();
|
||||
|
||||
private ExternalInetAddressMatcher() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(@Nullable InetAddress address) {
|
||||
return !this.internalMatcher.matches(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ExternalInetAddressMatcher";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A composite {@link InetAddressMatcher} that chains multiple matchers together. All
|
||||
* matchers must match for an address to be allowed. If report-only mode is enabled,
|
||||
* matching results are logged but all addresses are permitted.
|
||||
*
|
||||
* @author Gábor Vaspöri
|
||||
* @author Kian Jamali
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Rob Winch
|
||||
*/
|
||||
static final class CompositeInetAddressMatcher implements InetAddressMatcher {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(InetAddressMatcher.class);
|
||||
|
||||
private final List<InetAddressMatcher> matchers;
|
||||
|
||||
private final boolean reportOnly;
|
||||
|
||||
CompositeInetAddressMatcher(List<InetAddressMatcher> matchers, boolean reportOnly) {
|
||||
this.matchers = new ArrayList<>(matchers);
|
||||
this.reportOnly = reportOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(@Nullable InetAddress address) {
|
||||
boolean result = doMatch(address);
|
||||
return (this.reportOnly || result);
|
||||
}
|
||||
|
||||
private boolean doMatch(@Nullable InetAddress address) {
|
||||
for (InetAddressMatcher matcher : this.matchers) {
|
||||
if (!matcher.matches(address)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("InetAddress " + address + " blocked by " + matcher);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.web.util.matcher;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Utility class for parsing IP addresses.
|
||||
*
|
||||
* @author Luke Taylor
|
||||
* @author Steve Riesenberg
|
||||
* @author Andrey Litvitski
|
||||
* @author Rob Winch
|
||||
* @since 7.1
|
||||
*/
|
||||
final class InetAddressParser {
|
||||
|
||||
private static Pattern IPV4 = Pattern.compile("^\\d{1,3}(?:\\.\\d{1,3}){0,3}(?:/\\d{1,2})?$");
|
||||
|
||||
/**
|
||||
* Parses the given address string into an {@link InetAddress}.
|
||||
* @param address the IP address string to parse
|
||||
* @return the parsed {@link InetAddress}
|
||||
* @throws IllegalArgumentException if the address cannot be parsed or appears to be a
|
||||
* hostname
|
||||
*/
|
||||
static InetAddress parseAddress(String address) {
|
||||
assertNotHostName(address);
|
||||
try {
|
||||
return InetAddress.getByName(address);
|
||||
}
|
||||
catch (UnknownHostException ex) {
|
||||
throw new IllegalArgumentException("Failed to parse address '" + address + "'", ex);
|
||||
}
|
||||
}
|
||||
|
||||
static void assertNotHostName(String ipAddress) {
|
||||
Assert.isTrue(isIpAddress(ipAddress),
|
||||
() -> String.format("ipAddress %s doesn't look like an IP Address. Is it a host name?", ipAddress));
|
||||
}
|
||||
|
||||
private static boolean isIpAddress(String ipAddress) {
|
||||
if (!org.springframework.util.StringUtils.hasText(ipAddress)) {
|
||||
return false;
|
||||
}
|
||||
// @formatter:off
|
||||
return IPV4.matcher(ipAddress).matches()
|
||||
|| ipAddress.charAt(0) == '['
|
||||
|| ipAddress.charAt(0) == ':'
|
||||
|| Character.digit(ipAddress.charAt(0), 16) != -1
|
||||
&& ipAddress.indexOf(':') > 0;
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private InetAddressParser() {
|
||||
}
|
||||
|
||||
}
|
||||
@ -16,15 +16,8 @@
|
||||
|
||||
package org.springframework.security.web.util.matcher;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Matches a request based on IP Address or subnet mask matching against the remote
|
||||
@ -40,11 +33,7 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
public final class IpAddressMatcher implements RequestMatcher {
|
||||
|
||||
private static Pattern IPV4 = Pattern.compile("^\\d{1,3}(?:\\.\\d{1,3}){0,3}(?:/\\d{1,2})?$");
|
||||
|
||||
private final InetAddress requiredAddress;
|
||||
|
||||
private final int nMaskBits;
|
||||
private final InetAddressMatcher matcher;
|
||||
|
||||
/**
|
||||
* Takes a specific IP address or a range specified using the IP/Netmask (e.g.
|
||||
@ -53,89 +42,26 @@ public final class IpAddressMatcher implements RequestMatcher {
|
||||
* come.
|
||||
*/
|
||||
public IpAddressMatcher(String ipAddress) {
|
||||
Assert.hasText(ipAddress, "ipAddress cannot be empty");
|
||||
assertNotHostName(ipAddress);
|
||||
|
||||
String requiredAddress;
|
||||
int nMaskBits;
|
||||
if (ipAddress.indexOf('/') > 0) {
|
||||
String[] parts = Objects.requireNonNull(StringUtils.split(ipAddress, "/"));
|
||||
requiredAddress = parts[0];
|
||||
nMaskBits = Integer.parseInt(parts[1]);
|
||||
}
|
||||
else {
|
||||
requiredAddress = ipAddress;
|
||||
nMaskBits = -1;
|
||||
}
|
||||
this.requiredAddress = parseAddress(requiredAddress);
|
||||
this.nMaskBits = nMaskBits;
|
||||
Assert.isTrue(this.requiredAddress.getAddress().length * 8 >= this.nMaskBits, () -> String
|
||||
.format("IP address %s is too short for bitmask of length %d", requiredAddress, this.nMaskBits));
|
||||
this.matcher = new IpInetAddressMatcher(ipAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
return matches(request.getRemoteAddr());
|
||||
return this.matcher.matches(request.getRemoteAddr());
|
||||
}
|
||||
|
||||
public boolean matches(String ipAddress) {
|
||||
// Do not match null or blank address
|
||||
if (!StringUtils.hasText(ipAddress)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
assertNotHostName(ipAddress);
|
||||
InetAddress remoteAddress = parseAddress(ipAddress);
|
||||
if (!this.requiredAddress.getClass().equals(remoteAddress.getClass())) {
|
||||
return false;
|
||||
}
|
||||
if (this.nMaskBits < 0) {
|
||||
return remoteAddress.equals(this.requiredAddress);
|
||||
}
|
||||
byte[] remAddr = remoteAddress.getAddress();
|
||||
byte[] reqAddr = this.requiredAddress.getAddress();
|
||||
int nMaskFullBytes = this.nMaskBits / 8;
|
||||
for (int i = 0; i < nMaskFullBytes; i++) {
|
||||
if (remAddr[i] != reqAddr[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
byte finalByte = (byte) (0xFF00 >> (this.nMaskBits & 0x07));
|
||||
if (finalByte != 0) {
|
||||
return (remAddr[nMaskFullBytes] & finalByte) == (reqAddr[nMaskFullBytes] & finalByte);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void assertNotHostName(String ipAddress) {
|
||||
Assert.isTrue(isIpAddress(ipAddress),
|
||||
() -> String.format("ipAddress %s doesn't look like an IP Address. Is it a host name?", ipAddress));
|
||||
}
|
||||
|
||||
private static boolean isIpAddress(String ipAddress) {
|
||||
// @formatter:off
|
||||
return IPV4.matcher(ipAddress).matches()
|
||||
|| ipAddress.charAt(0) == '['
|
||||
|| ipAddress.charAt(0) == ':'
|
||||
|| Character.digit(ipAddress.charAt(0), 16) != -1
|
||||
&& ipAddress.indexOf(':') > 0;
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
private InetAddress parseAddress(String address) {
|
||||
try {
|
||||
return InetAddress.getByName(address);
|
||||
}
|
||||
catch (UnknownHostException ex) {
|
||||
throw new IllegalArgumentException("Failed to parse address '" + address + "'", ex);
|
||||
}
|
||||
/**
|
||||
* Checks if the given IP address string matches the configured address pattern.
|
||||
* @param ipAddress the IP address string to check (may be {@code null})
|
||||
* @return {@code true} if the address matches, {@code false} otherwise
|
||||
*/
|
||||
public boolean matches(@Nullable String ipAddress) {
|
||||
return this.matcher.matches(ipAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String hostAddress = this.requiredAddress.getHostAddress();
|
||||
return (this.nMaskBits < 0) ? "IpAddress [" + hostAddress + "]"
|
||||
: "IpAddress [" + hostAddress + "/" + this.nMaskBits + "]";
|
||||
return this.matcher.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.web.util.matcher;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Implementation of {@link InetAddressMatcher} that matches IP addresses with support for
|
||||
* CIDR notation (e.g., 192.168.1.0/24).
|
||||
* <p>
|
||||
* Both IPv4 and IPv6 addresses are supported. The matcher can be configured with either a
|
||||
* specific IP address or a subnet using CIDR notation.
|
||||
* <p>
|
||||
* The logic from this class was migrated from {@link IpAddressMatcher} to provide a more
|
||||
* general API that did not depend on the servlet APIs (e.g. HttpServletRequest).
|
||||
*
|
||||
* @author Luke Taylor
|
||||
* @author Steve Riesenberg
|
||||
* @author Andrey Litvitski
|
||||
* @since 7.1
|
||||
* @see IpAddressMatcher
|
||||
*/
|
||||
final class IpInetAddressMatcher implements InetAddressMatcher {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(IpAddressMatcher.class);
|
||||
|
||||
private final InetAddress requiredAddress;
|
||||
|
||||
private final int nMaskBits;
|
||||
|
||||
IpInetAddressMatcher(String ipAddress) {
|
||||
Assert.hasText(ipAddress, "ipAddress cannot be empty");
|
||||
String requiredAddress;
|
||||
int nMaskBits;
|
||||
if (ipAddress.indexOf('/') > 0) {
|
||||
String[] parts = Objects.requireNonNull(StringUtils.split(ipAddress, "/"));
|
||||
requiredAddress = parts[0];
|
||||
nMaskBits = Integer.parseInt(parts[1]);
|
||||
}
|
||||
else {
|
||||
requiredAddress = ipAddress;
|
||||
nMaskBits = -1;
|
||||
}
|
||||
this.requiredAddress = InetAddressParser.parseAddress(requiredAddress);
|
||||
this.nMaskBits = nMaskBits;
|
||||
Assert.isTrue(this.requiredAddress.getAddress().length * 8 >= this.nMaskBits, () -> String
|
||||
.format("IP address %s is too short for bitmask of length %d", requiredAddress, this.nMaskBits));
|
||||
}
|
||||
|
||||
private static InetAddress parse(String address) {
|
||||
try {
|
||||
InetAddress result = InetAddress.getByName(address);
|
||||
if (address.matches(".*[a-zA-Z\\-].*$") && !address.contains(":")) {
|
||||
logger.warn("Hostname '" + address + "' resolved to " + result.toString()
|
||||
+ " will be used on IP address matching");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (UnknownHostException ex) {
|
||||
throw new IllegalArgumentException(String.format("Failed to parse address '%s'", address), ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(@Nullable InetAddress toCheck) {
|
||||
if (toCheck == null) {
|
||||
return false;
|
||||
}
|
||||
if (this.nMaskBits < 0) {
|
||||
return toCheck.equals(this.requiredAddress);
|
||||
}
|
||||
byte[] remAddr = toCheck.getAddress();
|
||||
byte[] reqAddr = this.requiredAddress.getAddress();
|
||||
int nMaskFullBytes = this.nMaskBits / 8;
|
||||
byte finalByte = (byte) (0xFF00 >> (this.nMaskBits & 0x07));
|
||||
for (int i = 0; i < nMaskFullBytes; i++) {
|
||||
if (remAddr[i] != reqAddr[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (finalByte != 0) {
|
||||
return (remAddr[nMaskFullBytes] & finalByte) == (reqAddr[nMaskFullBytes] & finalByte);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String hostAddress = this.requiredAddress.getHostAddress();
|
||||
return (this.nMaskBits < 0) ? "IpAddress [" + hostAddress + "]"
|
||||
: "IpAddress [" + hostAddress + "/" + this.nMaskBits + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.web.util.matcher;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link InetAddressMatcher}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
class InetAddressMatcherTests {
|
||||
|
||||
@Test
|
||||
void matchesWhenStringValidIpv4ThenReturnsTrue() {
|
||||
InetAddressMatcher matcher = (address) -> address.getHostAddress().equals("192.168.1.1");
|
||||
assertThat(matcher.matches("192.168.1.1")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenStringValidIpv6ThenReturnsTrue() {
|
||||
InetAddressMatcher matcher = (address) -> address.getHostAddress().equals("fe80:0:0:0:21f:5bff:fe33:bd68");
|
||||
assertThat(matcher.matches("fe80::21f:5bff:fe33:bd68")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenStringNullThenReturnsFalse() {
|
||||
InetAddressMatcher matcher = (address) -> true;
|
||||
assertThat(matcher.matches((String) null)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenStringInvalidThenThrowsIllegalArgumentException() {
|
||||
InetAddressMatcher matcher = (address) -> true;
|
||||
assertThat(matcher.matches("192.168.1.1")).isTrue();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> matcher.matches("not.an.ip.address"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenStringMatchesPredicateThenReturnsTrue() {
|
||||
InetAddressMatcher matcher = (address) -> address.getHostAddress().startsWith("192.168");
|
||||
assertThat(matcher.matches("192.168.1.1")).isTrue();
|
||||
assertThat(matcher.matches("192.168.100.200")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenStringDoesNotMatchPredicateThenReturnsFalse() {
|
||||
InetAddressMatcher matcher = (address) -> address.getHostAddress().startsWith("192.168");
|
||||
assertThat(matcher.matches("10.0.0.1")).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,499 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.web.util.matcher;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link InetAddressMatchers}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
class InetAddressMatchersTests {
|
||||
|
||||
@Test
|
||||
void builderWhenInvokedThenReturnsBuilder() {
|
||||
assertThat(InetAddressMatchers.builder()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchExternalWhenInvokedThenReturnsBuilder() {
|
||||
InetAddressMatchers.Builder builder = InetAddressMatchers.matchExternal();
|
||||
assertThat(builder).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchInternalWhenInvokedThenReturnsBuilder() {
|
||||
InetAddressMatchers.Builder builder = InetAddressMatchers.matchInternal();
|
||||
assertThat(builder).isNotNull();
|
||||
}
|
||||
|
||||
@Nested
|
||||
class BuilderTests {
|
||||
|
||||
@Test
|
||||
void includeAddressesWhenNullThenThrowsIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> InetAddressMatchers.builder().includeAddresses(null))
|
||||
.withMessage("addresses cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void includeAddressesWhenEmptyListThenThrowsIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> InetAddressMatchers.builder().includeAddresses(List.of()))
|
||||
.withMessage("addresses cannot be empty");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "192.168.1.1", "192.168.1.2" })
|
||||
void includeAddressesWhenSingleAddressThenMatchesOnlyThatAddress(String testAddress) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.builder().includeAddresses(List.of("192.168.1.1")).build();
|
||||
InetAddress address = InetAddress.getByName(testAddress);
|
||||
boolean expected = testAddress.equals("192.168.1.1");
|
||||
assertThat(matcher.matches(address)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "192.168.1.1", "10.0.0.1", "8.8.8.8" })
|
||||
void includeAddressesWhenMultipleAddressesThenMatchesAny(String testAddress) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.builder()
|
||||
.includeAddresses(List.of("192.168.1.1", "10.0.0.1"))
|
||||
.build();
|
||||
InetAddress address = InetAddress.getByName(testAddress);
|
||||
boolean expected = testAddress.equals("192.168.1.1") || testAddress.equals("10.0.0.1");
|
||||
assertThat(matcher.matches(address)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "192.168.1.1", "192.168.1.255", "192.168.2.1" })
|
||||
void includeAddressesWhenCidrNotationThenMatchesSubnet(String testAddress) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.builder()
|
||||
.includeAddresses(List.of("192.168.1.0/24"))
|
||||
.build();
|
||||
InetAddress address = InetAddress.getByName(testAddress);
|
||||
boolean expected = testAddress.startsWith("192.168.1.");
|
||||
assertThat(matcher.matches(address)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void excludeAddressesWhenNullThenThrowsIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> InetAddressMatchers.builder().excludeAddresses(null))
|
||||
.withMessage("addresses cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void excludeAddressesWhenEmptyListThenThrowsIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> InetAddressMatchers.builder().excludeAddresses(List.of()))
|
||||
.withMessage("addresses cannot be empty");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "192.168.1.1", "192.168.1.2" })
|
||||
void excludeAddressesWhenSingleAddressThenBlocksOnlyThatAddress(String testAddress) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.builder().excludeAddresses(List.of("192.168.1.1")).build();
|
||||
InetAddress address = InetAddress.getByName(testAddress);
|
||||
boolean expected = !testAddress.equals("192.168.1.1");
|
||||
assertThat(matcher.matches(address)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "192.168.1.1", "10.0.0.1", "8.8.8.8" })
|
||||
void excludeAddressesWhenMultipleAddressesThenBlocksAll(String testAddress) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.builder()
|
||||
.excludeAddresses(List.of("192.168.1.1", "10.0.0.1"))
|
||||
.build();
|
||||
InetAddress address = InetAddress.getByName(testAddress);
|
||||
boolean expected = !testAddress.equals("192.168.1.1") && !testAddress.equals("10.0.0.1");
|
||||
assertThat(matcher.matches(address)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "192.168.1.1", "192.168.1.255", "192.168.2.1" })
|
||||
void excludeAddressesWhenCidrNotationThenBlocksSubnet(String testAddress) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.builder()
|
||||
.excludeAddresses(List.of("192.168.1.0/24"))
|
||||
.build();
|
||||
InetAddress address = InetAddress.getByName(testAddress);
|
||||
boolean expected = !testAddress.startsWith("192.168.1.");
|
||||
assertThat(matcher.matches(address)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "10.0.0.1", "192.168.1.1" })
|
||||
void matchAllWhenVarargsThenAddsMatchersToChain(String testAddress) throws Exception {
|
||||
InetAddressMatcher customMatcher = (address) -> address.getHostAddress().startsWith("10.");
|
||||
InetAddressMatcher matcher = InetAddressMatchers.builder().matchAll(customMatcher).build();
|
||||
InetAddress address = InetAddress.getByName(testAddress);
|
||||
boolean expected = testAddress.startsWith("10.");
|
||||
assertThat(matcher.matches(address)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchAllWhenNullVarargsThenThrowsIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> InetAddressMatchers.builder().matchAll((InetAddressMatcher[]) null))
|
||||
.withMessage("matchers cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchAllWhenEmptyVarargsThenThrowsIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> InetAddressMatchers.builder().matchAll(new InetAddressMatcher[0]))
|
||||
.withMessage("matchers cannot be empty");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "10.0.0.1", "10.0.0.2", "192.168.1.1" })
|
||||
void matchAllWhenMultipleMatchersThenAppliesAndLogic(String testAddress) throws Exception {
|
||||
InetAddressMatcher startsWithTen = (address) -> address.getHostAddress().startsWith("10.");
|
||||
InetAddressMatcher endsWithOne = (address) -> address.getHostAddress().endsWith(".1");
|
||||
InetAddressMatcher matcher = InetAddressMatchers.builder().matchAll(startsWithTen, endsWithOne).build();
|
||||
InetAddress address = InetAddress.getByName(testAddress);
|
||||
boolean expected = testAddress.startsWith("10.") && testAddress.endsWith(".1");
|
||||
assertThat(matcher.matches(address)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "192.168.1.1", "8.8.8.8" })
|
||||
void reportOnlyWhenSetThenAllowsAllAddresses(String testAddress) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.builder()
|
||||
.excludeAddresses(List.of("192.168.1.1"))
|
||||
.reportOnly()
|
||||
.build();
|
||||
InetAddress address = InetAddress.getByName(testAddress);
|
||||
assertThat(matcher.matches(address)).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "192.168.1.1", "192.168.1.100", "192.168.2.1" })
|
||||
void buildWhenMultipleMatchersThenAppliesAndLogic(String testAddress) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.builder()
|
||||
.includeAddresses(List.of("192.168.1.0/24"))
|
||||
.excludeAddresses(List.of("192.168.1.100"))
|
||||
.build();
|
||||
InetAddress address = InetAddress.getByName(testAddress);
|
||||
boolean expected = testAddress.startsWith("192.168.1.") && !testAddress.equals("192.168.1.100");
|
||||
assertThat(matcher.matches(address)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class IncludeListInetAddressMatcherTests {
|
||||
|
||||
@Test
|
||||
void constructorWhenNullListThenThrowsIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new InetAddressMatchers.IncludeListInetAddressMatcher(null))
|
||||
.withMessage("includeList cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWhenEmptyListThenThrowsIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new InetAddressMatchers.IncludeListInetAddressMatcher(List.of()))
|
||||
.withMessage("includeList cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenAddressInListThenReturnsTrue() throws Exception {
|
||||
String addressString = "192.168.1.1";
|
||||
InetAddressMatcher matcher = InetAddressMatchers.builder().includeAddresses(List.of(addressString)).build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(addressString))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenAddressNotInListThenReturnsFalse() throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.builder().includeAddresses(List.of("192.168.1.1")).build();
|
||||
assertThat(matcher.matches(InetAddress.getByName("192.168.1.2"))).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ExcludeListInetAddressMatcherTests {
|
||||
|
||||
@Test
|
||||
void constructorWhenNullListThenThrowsIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new InetAddressMatchers.ExcludeListInetAddressMatcher(null))
|
||||
.withMessage("excludeList cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWhenEmptyListThenThrowsIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new InetAddressMatchers.ExcludeListInetAddressMatcher(List.of()))
|
||||
.withMessage("excludeList cannot be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenAddressInListThenReturnsFalse() throws Exception {
|
||||
String addressString = "192.168.1.1";
|
||||
InetAddressMatcher matcher = InetAddressMatchers.builder().excludeAddresses(List.of(addressString)).build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(addressString))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenAddressNotInListThenReturnsTrue() throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.builder().excludeAddresses(List.of("192.168.1.1")).build();
|
||||
assertThat(matcher.matches(InetAddress.getByName("192.168.1.2"))).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class InternalInetAddressMatcherTests {
|
||||
|
||||
@Test
|
||||
void matchesWhenInetAddressNullThenReturnsFalse() {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches((InetAddress) null)).isFalse();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "127.0.0.1", "127.0.0.255" })
|
||||
void matchesWhenIpv4LoopbackThenReturnsTrue(String address) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv6LoopbackThenReturnsTrue() throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName("::1"))).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "10.0.0.1", "10.255.255.255" })
|
||||
void matchesWhenIpv4PrivateClass10ThenReturnsTrue(String address) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "192.168.0.1", "192.168.255.255" })
|
||||
void matchesWhenIpv4PrivateClass192ThenReturnsTrue(String address) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "169.254.0.0", "169.254.169.254", "169.254.255.255" })
|
||||
void matchesWhenIpv4LinkLocalThenReturnsTrue(String address) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "172.16.0.1", "172.16.255.255", "172.17.1.1", "172.31.255.255" })
|
||||
void matchesWhenIpv4PrivateClass172ThenReturnsTrue(String address) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "::ffff:192.168.1.1", "::ffff:169.254.169.254", "::ffff:10.0.0.1" })
|
||||
void matchesWhenIpv4MappedIpv6InternalThenReturnsTrue(String address) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "fc00::1", "fd00::1" })
|
||||
void matchesWhenIpv6UniqueLocalThenReturnsTrue(String address) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(
|
||||
strings = { "64:ff9b::10.0.0.1", "64:ff9b::127.0.0.1", "64:ff9b::192.168.1.1", "64:ff9b::172.16.0.1" })
|
||||
void matchesWhenIpv6TranslationWithInternalIpv4ThenReturnsTrue(String address) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "64:ff9b::192.0.2.1", "64:ff9b::192.167.1.1" })
|
||||
void matchesWhenIpv6TranslationWithIpv4StartsWith192ButNot168ThenReturnsFalse(String address) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(address))).isFalse();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "64:ff9b::172.16.0.1", "64:ff9b::172.16.255.255" })
|
||||
void matchesWhenIpv6TranslationWithIpv4StartsWith172And16ThenReturnsTrue(String address) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "64:ff9b::8.8.8.8", "64:ff9b::1.1.1.1" })
|
||||
void matchesWhenIpv6TranslationWithExternalIpv4ThenReturnsFalse(String address) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(address))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv6NonTranslationPrefixByte0ThenReturnsFalse() throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName("65:ff9b::10.0.0.1"))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv6NonTranslationPrefixByte1ThenReturnsFalse() throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName("64:fe9b::10.0.0.1"))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv6NonTranslationPrefixByte2ThenReturnsFalse() throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName("64:ff9a::10.0.0.1"))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv6NonTranslationPrefixByte3ThenReturnsFalse() throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName("64:ff9c::10.0.0.1"))).isFalse();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "8.8.8.8", "1.1.1.1" })
|
||||
void matchesWhenIpv4PublicThenReturnsFalse(String address) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(address))).isFalse();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "192.0.2.1", "192.167.1.1", "192.169.1.1" })
|
||||
void matchesWhenIpv4StartsWith192ButNot168ThenReturnsFalse(String address) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(address))).isFalse();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "172.15.1.1", "172.32.1.1" })
|
||||
void matchesWhenIpv4StartsWith172ButNotPrivate16To31ThenReturnsFalse(String address) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(address))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv6PublicThenReturnsFalse() throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchInternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName("2001:4860:4860::8888"))).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ExternalInetAddressMatcherTests {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "8.8.8.8", "1.1.1.1" })
|
||||
void matchesWhenIpv4PublicThenReturnsTrue(String address) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchExternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(address))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv6PublicThenReturnsTrue() throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchExternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName("2001:4860:4860::8888"))).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "192.168.1.1", "10.0.0.1", "172.16.0.1" })
|
||||
void matchesWhenIpv4PrivateThenReturnsFalse(String address) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchExternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(address))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv4LoopbackThenReturnsFalse() throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchExternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName("127.0.0.1"))).isFalse();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "169.254.0.0", "169.254.169.254", "169.254.255.255" })
|
||||
void matchesWhenIpv4LinkLocalThenReturnsFalse(String address) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchExternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(address))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv6LoopbackThenReturnsFalse() throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchExternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName("::1"))).isFalse();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "fc00::1", "fd00::1" })
|
||||
void matchesWhenIpv6UniqueLocalThenReturnsFalse(String address) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.matchExternal().build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(address))).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class CompositeInetAddressMatcherTests {
|
||||
|
||||
@Test
|
||||
void matchesWhenAllMatchersTrueThenReturnsTrue() throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.builder()
|
||||
.includeAddresses(List.of("192.168.1.0/24"))
|
||||
.matchAll((address) -> address.getHostAddress().endsWith(".1"))
|
||||
.build();
|
||||
assertThat(matcher.matches(InetAddress.getByName("192.168.1.1"))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenOneMatcherFalseThenReturnsFalse() throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.builder()
|
||||
.includeAddresses(List.of("192.168.1.0/24"))
|
||||
.matchAll((address) -> address.getHostAddress().endsWith(".1"))
|
||||
.build();
|
||||
assertThat(matcher.matches(InetAddress.getByName("192.168.1.2"))).isFalse();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "192.168.1.1", "8.8.8.8" })
|
||||
void matchesWhenReportOnlyThenAlwaysReturnsTrue(String testAddress) throws Exception {
|
||||
InetAddressMatcher matcher = InetAddressMatchers.builder()
|
||||
.excludeAddresses(List.of("192.168.1.1"))
|
||||
.reportOnly()
|
||||
.build();
|
||||
assertThat(matcher.matches(InetAddress.getByName(testAddress))).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -154,6 +154,12 @@ public class IpAddressMatcherTests {
|
||||
.withMessage("ipAddress cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isIpAddressWhenEmptyOrBlankThenThrowsIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new IpAddressMatcher(" "))
|
||||
.withMessage("ipAddress cannot be empty");
|
||||
}
|
||||
|
||||
// gh-16795
|
||||
@Test
|
||||
public void toStringWhenCidrIsProvidedThenReturnsIpAddressWithCidr() {
|
||||
|
||||
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2004-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.security.web.util.matcher;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link IpInetAddressMatcher}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
class IpInetAddressMatcherTests {
|
||||
|
||||
@Test
|
||||
void constructorWhenNullIpAddressThenThrowsIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new IpInetAddressMatcher(null))
|
||||
.withMessage("ipAddress cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWhenEmptyIpAddressThenThrowsIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new IpInetAddressMatcher(""))
|
||||
.withMessage("ipAddress cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorWhenHostnameThenThrowsIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new IpInetAddressMatcher("example.com"))
|
||||
.withMessageContaining("doesn't look like an IP Address");
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv4ExactMatchThenReturnsTrue() throws Exception {
|
||||
IpInetAddressMatcher matcher = new IpInetAddressMatcher("192.168.1.1");
|
||||
assertThat(matcher.matches(InetAddress.getByName("192.168.1.1"))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv4NoMatchThenReturnsFalse() throws Exception {
|
||||
IpInetAddressMatcher matcher = new IpInetAddressMatcher("192.168.1.1");
|
||||
assertThat(matcher.matches(InetAddress.getByName("192.168.1.2"))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv6ExactMatchThenReturnsTrue() throws Exception {
|
||||
IpInetAddressMatcher matcher = new IpInetAddressMatcher("fe80::21f:5bff:fe33:bd68");
|
||||
assertThat(matcher.matches(InetAddress.getByName("fe80::21f:5bff:fe33:bd68"))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv6NoMatchThenReturnsFalse() throws Exception {
|
||||
IpInetAddressMatcher matcher = new IpInetAddressMatcher("fe80::21f:5bff:fe33:bd68");
|
||||
assertThat(matcher.matches(InetAddress.getByName("fe80::21f:5bff:fe33:bd69"))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv4WithCidrMatchesSubnetThenReturnsTrue() throws Exception {
|
||||
IpInetAddressMatcher matcher = new IpInetAddressMatcher("192.168.1.0/24");
|
||||
assertThat(matcher.matches(InetAddress.getByName("192.168.1.1"))).isTrue();
|
||||
assertThat(matcher.matches(InetAddress.getByName("192.168.1.255"))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv4WithCidrOutsideSubnetThenReturnsFalse() throws Exception {
|
||||
IpInetAddressMatcher matcher = new IpInetAddressMatcher("192.168.1.0/24");
|
||||
assertThat(matcher.matches(InetAddress.getByName("192.168.2.1"))).isFalse();
|
||||
assertThat(matcher.matches(InetAddress.getByName("192.168.0.255"))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv6WithCidrMatchesSubnetThenReturnsTrue() throws Exception {
|
||||
IpInetAddressMatcher matcher = new IpInetAddressMatcher("2001:db8::/48");
|
||||
assertThat(matcher.matches(InetAddress.getByName("2001:db8:0:0:0:0:0:0"))).isTrue();
|
||||
assertThat(matcher.matches(InetAddress.getByName("2001:db8:0:ffff:ffff:ffff:ffff:ffff"))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv6WithCidrOutsideSubnetThenReturnsFalse() throws Exception {
|
||||
IpInetAddressMatcher matcher = new IpInetAddressMatcher("2001:db8::/48");
|
||||
assertThat(matcher.matches(InetAddress.getByName("2001:db8:1:0:0:0:0:0"))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv4AndIpv6AddressThenReturnsFalse() throws Exception {
|
||||
IpInetAddressMatcher matcher = new IpInetAddressMatcher("192.168.1.1");
|
||||
assertThat(matcher.matches(InetAddress.getByName("fe80::21f:5bff:fe33:bd68"))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenIpv6AndIpv4AddressThenReturnsFalse() throws Exception {
|
||||
IpInetAddressMatcher matcher = new IpInetAddressMatcher("fe80::21f:5bff:fe33:bd68");
|
||||
assertThat(matcher.matches(InetAddress.getByName("192.168.1.1"))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenStringIpv4MatchThenReturnsTrue() {
|
||||
IpInetAddressMatcher matcher = new IpInetAddressMatcher("192.168.1.1");
|
||||
assertThat(matcher.matches("192.168.1.1")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenStringIpv4NoMatchThenReturnsFalse() {
|
||||
IpInetAddressMatcher matcher = new IpInetAddressMatcher("192.168.1.1");
|
||||
assertThat(matcher.matches("192.168.1.2")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenStringNullThenReturnsFalse() {
|
||||
IpInetAddressMatcher matcher = new IpInetAddressMatcher("192.168.1.1");
|
||||
assertThat(matcher.matches((String) null)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenInetAddressNullThenReturnsFalse() {
|
||||
IpInetAddressMatcher matcher = new IpInetAddressMatcher("192.168.1.1");
|
||||
assertThat(matcher.matches((InetAddress) null)).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user