/*
* Copyright (C) 2024-2026 Ignite Realtime Foundation. All rights reserved.
*
* 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
*
* http://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.jivesoftware.util;
import com.github.jgonian.ipmath.Ipv4;
import com.github.jgonian.ipmath.Ipv4Range;
import com.github.jgonian.ipmath.Ipv6;
import com.github.jgonian.ipmath.Ipv6Range;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.math.BigInteger;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Various utility methods for working with (string representations of) IP-addresses.
*
* @author Guus der Kinderen, guus@goodbytes.nl
*/
public class IpUtils
{
/**
* Checks if the provided address is matched by any of the addresses or address ranges.
*
* @param address The address to look up.
* @param addressesAndRanges the values to compare the provided address with.
* @return true if the address is found in the addresses or ranges, otherwise false.
*/
public static boolean isAddressInAnyOf(final @Nonnull byte[] address, final @Nonnull Set addressesAndRanges)
{
if (address.length == 4) {
return isAddressInAnyOf(from4(address), addressesAndRanges);
}
if (address.length == 16) {
return isAddressInAnyOf(from16(address), addressesAndRanges);
}
throw new IllegalArgumentException("Unrecognized address type: " + Arrays.toString(address));
}
/**
* Checks if the provided address is matched by any of the addresses or address ranges.
*
* @param address The address to look up.
* @param addressesAndRanges the values to compare the provided address with.
* @return true if the address is found in the addresses or ranges, otherwise false.
*/
public static boolean isAddressInAnyOf(final @Nonnull String address, final @Nonnull Set addressesAndRanges)
{
if (isValidIpv4(address)) {
return isAddressInAnyOf(Ipv4.of(address), addressesAndRanges);
}
if (isValidIpv6(address)) {
return isAddressInAnyOf(Ipv6.of(stripIpv6ZoneId(address)), addressesAndRanges);
}
throw new IllegalArgumentException("Unrecognized address type: " + address);
}
/**
* Checks if the provided address is matched by any of the addresses or address ranges.
*
* @param address The address to look up.
* @param addressesAndRanges the values to compare the provided address with.
* @return true if the address is found in the addresses or ranges, otherwise false.
*/
public static boolean isAddressInAnyOf(final @Nonnull InetAddress address, final @Nonnull Set addressesAndRanges)
{
if (address instanceof Inet4Address) {
return isAddressInAnyOf(from((Inet4Address) address), addressesAndRanges);
}
if (address instanceof Inet6Address) {
return isAddressInAnyOf(from((Inet6Address) address), addressesAndRanges);
}
throw new IllegalArgumentException("Unrecognized address type: " + address);
}
/**
* Checks if the provided address is matched by any of the addresses or address ranges.
*
* @param address The address to look up.
* @param addressesAndRanges the values to compare the provided address with.
* @return true if the address is found in the addresses or ranges, otherwise false.
*/
public static boolean isAddressInAnyOf(final @Nonnull Ipv4 address, final @Nonnull Set addressesAndRanges)
{
final Set addresses = addressesAndRanges.stream().filter(IpUtils::isValidIpv4).map(Ipv4::of).collect(Collectors.toSet());
final Set ranges = addressesAndRanges.stream().filter(IpUtils::isValidIpv4Range).map(IpUtils::convertIpv4WildcardRangeToCidrNotation).map(Ipv4Range::parse).collect(Collectors.toSet());
return isAddressInAnyOf(address, addresses, ranges);
}
/**
* Checks if the provided address is matched by any of the addresses or address ranges.
*
* @param address The address to look up.
* @param addressesAndRanges the values to compare the provided address with.
* @return true if the address is found in the addresses or ranges, otherwise false.
*/
public static boolean isAddressInAnyOf(final @Nonnull Ipv6 address, final @Nonnull Set addressesAndRanges)
{
final Set addresses = addressesAndRanges.stream().filter(IpUtils::isValidIpv6).map(IpUtils::stripIpv6ZoneId).map(Ipv6::of).collect(Collectors.toSet());
final Set ranges = addressesAndRanges.stream().filter(IpUtils::isValidIpv6Range).map(Ipv6Range::parse).collect(Collectors.toSet());
return isAddressInAnyOf(address, addresses, ranges);
}
/**
* Checks if the provided address is matched by any of the addresses or address ranges.
*
* @param address The address to look up.
* @param addresses the address-values to compare the provided address with.
* @param ranges the range-values to compare the provided address with.
* @return true if the address is found in the addresses or ranges, otherwise false.
*/
public static boolean isAddressInAnyOf(final @Nonnull Ipv4 address, final @Nonnull Set addresses, final @Nonnull Set ranges)
{
return addresses.stream().anyMatch(a->a.equals(address)) || ranges.stream().anyMatch(r->r.contains(address));
}
/**
* Checks if the provided address is matched by any of the addresses or address ranges.
*
* @param address The address to look up.
* @param addresses the address-values to compare the provided address with.
* @param ranges the range-values to compare the provided address with.
* @return true if the address is found in the addresses or ranges, otherwise false.
*/
public static boolean isAddressInAnyOf(final @Nonnull Ipv6 address, final @Nonnull Set addresses, final @Nonnull Set ranges)
{
return addresses.stream().anyMatch(a->a.equals(address)) || ranges.stream().anyMatch(r->r.contains(address));
}
// Replace this with API introduced in https://github.com/jgonian/commons-ip-math/pull/32
static Ipv4 from(final @Nonnull Inet4Address address) {
return from4(address.getAddress());
}
// Replace this with API introduced in https://github.com/jgonian/commons-ip-math/pull/32
static Ipv4 from4(final @Nonnull byte[] octets) {
long result = 0;
for (byte octet : octets) {
result = (result << 8) | Byte.toUnsignedInt(octet);
}
return Ipv4.of(result);
}
// Replace this with API introduced in https://github.com/jgonian/commons-ip-math/pull/32
static Ipv6 from(final @Nonnull Inet6Address address) {
return from16(address.getAddress());
}
// Replace this with API introduced in https://github.com/jgonian/commons-ip-math/pull/32
static Ipv6 from16(final @Nonnull byte[] octets) {
BigInteger result = BigInteger.ZERO;
for (byte octet : octets) {
result = result.shiftLeft(8).add(BigInteger.valueOf(Byte.toUnsignedInt(octet)));
}
return Ipv6.of(result);
}
/**
* When the provided input is an IPv6 literal that is enclosed in brackets (the [] style as expressed in
* https://tools.ietf.org/html/rfc2732 and https://tools.ietf.org/html/rfc6874), this method returns the value
* stripped from those brackets (the IPv6 address, instead of the literal). In all other cases, the input value is
* returned.
*
* @param address The value from which to strip brackets.
* @return the input value, stripped from brackets if applicable.
*/
@Nonnull
public static String removeBracketsFromIpv6Address(@Nonnull final String address)
{
final String result;
if (address.startsWith("[") && address.endsWith("]")) {
result = address.substring(1, address.length()-1);
try {
Ipv6.parse(result);
// The remainder is a valid IPv6 address. Return the original value.
return result;
} catch (IllegalArgumentException e) {
// The remainder isn't a valid IPv6 address. Return the original value.
return address;
}
}
// Not a bracket-enclosed string. Return the original input.
return address;
}
/**
* Replaces a representation of an IPv4 network range that is using wildcards (eg: 192.*.*.*) with a
* notation that is CIDR-based (eg: 192.0.0.0/8).
*
* When the string cannot be transformed, the original string is returned.
*
* @param value The network range to transform
* @return The transformed range.
*/
public static String convertIpv4WildcardRangeToCidrNotation(final @Nonnull String value)
{
return value.replace(".*.*.*", ".0.0.0/8")
.replace(".*.*", ".0.0/16")
.replace(".*", ".0/24");
}
/**
* Checks if the provided value is a representation of an IPv4 address.
*
* @param value the value to check
* @return true if the provided value is an IPv4 address, otherwise false.
*/
public static boolean isValidIpv4(@Nullable final String value)
{
if (value == null) {
return false;
}
try {
Ipv4.parse(value);
} catch (IllegalArgumentException e) {
return false;
}
return true;
}
/**
* Checks if the provided value is a representation of an IPv4 address range.
*
* @param value the value to check
* @return true if the provided value is an IPv4 address range, otherwise false.
*/
public static boolean isValidIpv4Range(@Nullable final String value)
{
if (value == null) {
return false;
}
try {
Ipv4Range.parse(convertIpv4WildcardRangeToCidrNotation(value));
} catch (IllegalArgumentException e) {
return false;
}
return true;
}
/**
* Checks if the provided value is a representation of an IPv6 address.
*
* An optional zone/scope ID suffix (e.g. {@code %eth0} or {@code %1} as appended by
* {@link java.net.InetAddress#getHostAddress()}) is silently stripped before the check so that
* scoped link-local addresses such as {@code fe80::1%eth0} are recognised as valid.
*
* @param value the value to check
* @return true if the provided value is an IPv6 address, otherwise false.
*/
public static boolean isValidIpv6(@Nullable final String value)
{
if (value == null) {
return false;
}
try {
Ipv6.parse(stripIpv6ZoneId(value));
} catch (IllegalArgumentException e) {
return false;
}
return true;
}
/**
* Strips the IPv6 zone/scope ID suffix from an address string if one is present.
*
* For example, {@code "fe80::1%eth0"} becomes {@code "fe80::1"}. Strings that contain no {@code '%'} character are
* returned unchanged. This handles the suffix that {@link java.net.InetAddress#getHostAddress()} appends for scoped
* IPv6 addresses.
*
* @param address the address string to normalise; must not be {@code null}
* @return the address string with any zone/scope ID removed
*/
private static String stripIpv6ZoneId(@Nonnull final String address)
{
final int zoneIndex = address.indexOf('%');
return zoneIndex >= 0 ? address.substring(0, zoneIndex) : address;
}
/**
* Checks if the provided value is a representation of an IPv6 address range.
*
* @param value the value to check
* @return true if the provided value is an IPv6 address range, otherwise false.
*/
public static boolean isValidIpv6Range(@Nullable final String value)
{
if (value == null) {
return false;
}
try {
Ipv6Range.parse(value);
} catch (IllegalArgumentException e) {
return false;
}
return true;
}
/**
* Checks if the provided value is a representation of an IP address.
*
* @param value the value to check
* @return true if the provided value is an IP address, otherwise false.
*/
public static boolean isValidIpAddress(@Nullable final String value)
{
return isValidIpv4(value) || isValidIpv6(value);
}
/**
* Checks if the provided value is a representation of an IP address range.
*
* @param value the value to check
* @return true if the provided value is an IP address range, otherwise false.
*/
public static boolean isValidIpRange(@Nullable final String value)
{
return isValidIpv4Range(value) || isValidIpv6Range(value);
}
/**
* Checks if the provided value is a representation of an IP address or an IP address range.
*
* @param value the value to check
* @return true if the provided value is an IP address or IP address range, otherwise false.
*/
public static boolean isValidIpAddressOrRange(@Nullable final String value)
{
return isValidIpAddress(value) || isValidIpRange(value);
}
}