+ * For large ranges, you can iterate or stream over the addresses instead using {@link #iterableAddressStrings()} or {@link #streamAddressStrings()}. + *
+ * + * @return all addresses in this subnet. + * @see #iterableAddressStrings() + * @see #streamAddressStrings() + */ + public String[] getAllAddresses() { + final int ct = getAddressCount(); + final String[] addresses = new String[ct]; + if (ct == 0) { + return addresses; + } + final int high = high(); + for (int add = low(), j = 0; add <= high; ++add, ++j) { + addresses[j] = format(toArray4(add)); + } + return addresses; + } + + /** + * Gets the broadcast address for this subnet. + * + * @return the broadcast address for this subnet. + */ + public String getBroadcastAddress() { + return format(toArray4(broadcast)); + } + + /** + * Gets the CIDR signature for this subnet. + * + * @return the CIDR signature for this subnet. + */ + public String getCidrSignature() { + return format(toArray4(address)) + "/" + Integer.bitCount(netmask); + } + + /** + * Gets the high address as a dotted IP address. Will be zero for CIDR/31 and CIDR/32 if the inclusive flag is false. + * + * @return the IP address in dotted format, may be "0.0.0.0" if there is no valid address + */ + public String getHighAddress() { + return format(toArray4(high())); + } + + /** + * Gets the low address as a dotted IP address. Will be zero for CIDR/31 and CIDR/32 if the inclusive flag is false. + * + * @return the IP address in dotted format, may be "0.0.0.0" if there is no valid address + */ + public String getLowAddress() { + return format(toArray4(low())); + } + + /** + * Gets the network mask for this subnet. + * + * @return the network mask for this subnet. + */ + public String getNetmask() { + return format(toArray4(netmask)); + } + + /** + * Gets the network address for this subnet. + * + * @return the network address for this subnet. + */ + public String getNetworkAddress() { + return format(toArray4(network)); + } + + /** + * Gets the next address for this subnet. + * + * @return the next address for this subnet. + */ + public String getNextAddress() { + return format(toArray4(address + 1)); + } + + /** + * Gets the previous address for this subnet. + * + * @return the previous address for this subnet. + */ + public String getPreviousAddress() { + return format(toArray4(address - 1)); + } + + private int high() { + return isInclusiveHostCount() ? broadcast : broadcastLong() - networkLong() > 1 ? broadcast - 1 : 0; + } + + /** + * Tests if the parameter {@code address} is in the range of usable endpoint addresses for this subnet. This excludes the network and broadcast + * addresses by default. Use {@link SubnetUtils#setInclusiveHostCount(boolean)} to change this. + * + * @param address the address to check + * @return true if it is in range + * @since 3.4 (made public) + */ + public boolean isInRange(final int address) { + if (address == 0) { // cannot ever be in range; rejecting now avoids problems with CIDR/31,32 + return false; + } + final long addLong = address & UNSIGNED_INT_MASK; + final long lowLong = low() & UNSIGNED_INT_MASK; + final long highLong = high() & UNSIGNED_INT_MASK; + return addLong >= lowLong && addLong <= highLong; + } + + /** + * Tests if the parameter {@code address} is in the range of usable endpoint addresses for this subnet. This excludes the network and broadcast + * addresses. Use {@link SubnetUtils#setInclusiveHostCount(boolean)} to change this. + * + * @param address A dot-delimited IPv4 address, e.g. "192.168.0.1" + * @return True if in range, false otherwise + */ + public boolean isInRange(final String address) { + return isInRange(toInteger(address)); + } + + /** + * Creates a new Iterable of address Strings. + * + * @return a new Iterable of address Strings + * @see #getAllAddresses() + * @see #streamAddressStrings() + * @since 3.12.0 + */ + public Iterable+* This is the IPv6 equivalent of {@link SubnetUtils}. Addresses are parsed and formatted +* using {@link InetAddress}, which accepts the text representations described in +* RFC 5952. +*
+* +* This class is extracted from Apache commons-net project +* @see SubnetUtils +* @see RFC 5952 - A Recommendation for IPv6 Address Text Representation +* @since 3.13.0 +*/ +public class SubnetUtils6 { + + /** + * Contains IPv6 subnet summary information. + */ + public final class SubnetInfo { + + private SubnetInfo() { } + + /** + * Gets the address used to initialize this subnet. + * + * @return the address as a string in standard IPv6 format. + */ + public String getAddress() { + return format(address); + } + + /** + * Gets the count of available addresses in this subnet. + *+ * For IPv6, this can be astronomically large. A /64 subnet has 2^64 addresses. + *
+ * + * @return the count of addresses as a BigInteger. + */ + public BigInteger getAddressCount() { + // 2^(128 - prefixLength) + return TWO.pow(NBITS - prefixLength); + } + + /** + * Gets the CIDR notation for this subnet. + * + * @return the CIDR signature (e.g., "2001:db8::1/64"). + */ + public String getCidrSignature() { + return format(address) + "/" + prefixLength; + } + + /** + * Gets the highest address in this subnet. + * + * @return the high address as a string in standard IPv6 format. + */ + public String getHighAddress() { + return format(high); + } + + /** + * Gets the lowest address in this subnet (the network address). + * + * @return the low address as a string in standard IPv6 format. + */ + public String getLowAddress() { + return format(network); + } + + /** + * Gets the network address for this subnet. + * + * @return the network address as a string in standard IPv6 format. + */ + public String getNetworkAddress() { + return format(network); + } + + /** + * Gets the prefix length for this subnet. + * + * @return the prefix length (0-128). + */ + public int getPrefixLength() { + return prefixLength; + } + + /** + * Tests if the given address is within this subnet range. + * + * @param addr the IPv6 address to test (as a BigInteger). + * @return true if the address is in range. + */ + public boolean isInRange(final BigInteger addr) { + if (addr == null) { + return false; + } + return addr.compareTo(network) >= 0 && addr.compareTo(high) <= 0; + } + + /** + * Tests if the given address is within this subnet range. + * + * @param addr the IPv6 address to test as a byte array (16 bytes). + * @return true if the address is in range. + */ + public boolean isInRange(final byte[] addr) { + if (addr == null || addr.length != 16) { + return false; + } + return isInRange(new BigInteger(1, addr)); + } + + /** + * Tests if the given address is within this subnet range. + * + * @param addr the IPv6 address to test. + * @return true if the address is in range. + */ + public boolean isInRange(final Inet6Address addr) { + if (addr == null) { + return false; + } + return isInRange(addr.getAddress()); + } + + /** + * Tests if the given address is within this subnet range. + * + * @param addr the IPv6 address to test as a string. + * @return true if the address is in range. + * @throws IllegalArgumentException if the address cannot be parsed. + */ + public boolean isInRange(final String addr) { + return isInRange(toBytes(addr)); + } + + /** + * Returns a summary of this subnet for debugging. + * + * @return a multi-line debug string summarizing this subnet. + */ + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("CIDR Signature:\t[").append(getCidrSignature()).append("]\n") + .append(" Network: [").append(getNetworkAddress()).append("]\n") + .append(" First address: [").append(getLowAddress()).append("]\n") + .append(" Last address: [").append(getHighAddress()).append("]\n") + .append(" Address Count: [").append(getAddressCount()).append("]\n"); + return buf.toString(); + } + } + + private static final int NBITS = 128; + private static final String PARSE_FAIL = "Could not parse [%s]"; + private static final BigInteger TWO = BigInteger.valueOf(2); + private static final BigInteger MAX_VALUE = TWO.pow(NBITS).subtract(BigInteger.ONE); + + /** + * Formats a BigInteger as an IPv6 address string using {@link InetAddress#getHostAddress()}. + * + * @param addr the address as a BigInteger. + * @return the formatted IPv6 address string. + * @see RFC 5952 + */ + private static String format(final BigInteger addr) { + final byte[] bytes = toByteArray16(addr); + try { + return InetAddress.getByAddress(bytes).getHostAddress(); + } catch (final UnknownHostException e) { + // Should never happen with a valid 16-byte array + throw new IllegalStateException("Unexpected error formatting IPv6 address", e); + } + } + + /** + * Converts a BigInteger to a 16-byte array, padding with leading zeros if necessary. + * + * @param value the BigInteger to convert. + * @return a 16-byte array. + */ + private static byte[] toByteArray16(final BigInteger value) { + final byte[] raw = value.toByteArray(); + if (raw.length == 16) { + return raw; + } + final byte[] result = new byte[16]; + if (raw.length > 16) { + // BigInteger may have a leading sign byte; skip it + System.arraycopy(raw, raw.length - 16, result, 0, 16); + } else { + // Pad with leading zeros + System.arraycopy(raw, 0, result, 16 - raw.length, raw.length); + } + return result; + } + + /** + * Parses an IPv6 address string to a byte array. + * + * @param address the IPv6 address string. + * @return the 16-byte representation. + * @throws IllegalArgumentException if the address cannot be parsed. + */ + private static byte[] toBytes(final String address) { + try { + final InetAddress inetAddr = InetAddress.getByName(address); + if (inetAddr instanceof Inet6Address) { + return inetAddr.getAddress(); + } + throw new IllegalArgumentException(String.format(PARSE_FAIL, address) + " - not an IPv6 address"); + } catch (final UnknownHostException e) { + throw new IllegalArgumentException(String.format(PARSE_FAIL, address), e); + } + } + + private final BigInteger address; + private final BigInteger high; + private final BigInteger network; + private final int prefixLength; + + /** + * Constructs an instance from a CIDR-notation string, e.g., "2001:db8::1/64". + * + * @param cidrNotation a CIDR-notation string, e.g., "2001:db8::1/64". + * @throws IllegalArgumentException if the parameter is invalid. + */ + public SubnetUtils6(final String cidrNotation) { + if (cidrNotation == null) { + throw new IllegalArgumentException(String.format(PARSE_FAIL, "null") + " - null input"); + } + + final int slashIndex = cidrNotation.indexOf('/'); + if (slashIndex < 0) { + throw new IllegalArgumentException(String.format(PARSE_FAIL, cidrNotation) + " - missing prefix length"); + } + + final String addressPart = cidrNotation.substring(0, slashIndex); + final String prefixPart = cidrNotation.substring(slashIndex + 1); + + // Parse and validate prefix length + try { + this.prefixLength = Integer.parseInt(prefixPart); + } catch (final NumberFormatException e) { + throw new IllegalArgumentException(String.format(PARSE_FAIL, cidrNotation) + " - invalid prefix length", e); + } + + if (this.prefixLength < 0 || this.prefixLength > NBITS) { + throw new IllegalArgumentException(String.format(PARSE_FAIL, cidrNotation) + + " - prefix length must be between 0 and " + NBITS); + } + + // Parse and validate IPv6 address + final byte[] addressBytes = toBytes(addressPart); + this.address = new BigInteger(1, addressBytes); + + // Create netmask: prefixLength 1-bits followed by (128 - prefixLength) 0-bits + final BigInteger netmask; + if (this.prefixLength == 0) { + netmask = BigInteger.ZERO; + } else { + netmask = MAX_VALUE.shiftLeft(NBITS - this.prefixLength).and(MAX_VALUE); + } + + // Calculate network address + this.network = this.address.and(netmask); + + // Calculate the highest address in the range + final BigInteger hostmask = MAX_VALUE.xor(netmask); + this.high = this.network.or(hostmask); + } + + /** + * Constructs an instance from an IPv6 address and prefix length. + * + * @param address an IPv6 address, e.g., "2001:db8::1". + * @param prefixLength the prefix length (0-128). + * @throws IllegalArgumentException if the parameters are invalid. + */ + public SubnetUtils6(final String address, final int prefixLength) { + this(address + "/" + prefixLength); + } + + /** + * Gets a {@link SubnetInfo} instance that contains subnet-specific statistics. + * + * @return a new SubnetInfo instance. + */ + public SubnetInfo getInfo() { + return new SubnetInfo(); + } + + /** + * Returns a summary of this subnet for debugging. + *+ * Delegates to {@link SubnetInfo#toString()}. This is a diagnostic format and is not suitable for parsing. + * Use {@link SubnetInfo#getCidrSignature()} to obtain a string that can be fed back into + * {@link #SubnetUtils6(String)}. + *
+ * + * @return a multi-line debug string summarizing this subnet. + */ + @Override + public String toString() { + return getInfo().toString(); + } +} diff --git a/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv4Test.java b/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv4Test.java index db3e31429..9ed6612bf 100644 --- a/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv4Test.java +++ b/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv4Test.java @@ -30,7 +30,6 @@ import org.junit.Test; /** - * TODO Add documentation * * @author Apache MINA Project */ diff --git a/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv6Test.java b/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv6Test.java index 5d06601af..76b1ca2d1 100644 --- a/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv6Test.java +++ b/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv6Test.java @@ -1,49 +1,120 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * - */ +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you 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.apache.mina.filter.firewall; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; import org.junit.Test; /** - * TODO Add documentation - * - * @author Apache MINA Project - */ +* +* @author Apache MINA Project +*/ public class SubnetIPv6Test { - // Test Data - private static final String TEST_V6ADDRESS = "1080:0:0:0:8:800:200C:417A"; - @Test public void testIPv6() throws UnknownHostException { - InetAddress a = InetAddress.getByName(TEST_V6ADDRESS); - assertTrue(a instanceof Inet6Address); + Subnet subnet = new Subnet(InetAddress.getByName("2001:db8::"), 32); + assertTrue(!subnet.inSubnet(InetAddress.getByName("2001:db7:ffff:ffff:ffff:ffff:ffff:ffff"))); + assertTrue(!subnet.inSubnet(InetAddress.getByName("2001:db9::"))); + assertTrue(subnet.inSubnet(InetAddress.getByName("2001:db8::1"))); + assertTrue(subnet.inSubnet(InetAddress.getByName("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"))); + + } + + @Test + public void test32() throws UnknownHostException { + InetAddress a = InetAddress.getByName("2001:db8::"); + InetAddress b = InetAddress.getByName("2001:db8::1"); + InetAddress c = InetAddress.getByName("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"); + InetAddress d = InetAddress.getByName("2001:db7:ffff:ffff:ffff:ffff:ffff:ffff"); + InetAddress e = InetAddress.getByName("2001:db9::"); + + Subnet mask = new Subnet(a, 32); + + assertTrue(mask.inSubnet(a)); + assertTrue(mask.inSubnet(b)); + assertTrue(mask.inSubnet(c)); + assertFalse(mask.inSubnet(d)); + assertFalse(mask.inSubnet(e)); + } + + @Test + public void test96() throws UnknownHostException { + InetAddress a = InetAddress.getByName("2001:db8:dead:beef:abcd:abcd::"); + InetAddress b = InetAddress.getByName("2001:db8:dead:beef:abcd:abcd::"); + InetAddress c = InetAddress.getByName("2001:db8:dead:beef:abcd:abcd:ffff:ffff"); + InetAddress d = InetAddress.getByName("2001:db8:dead:beef:abcd:abce::"); + InetAddress e = InetAddress.getByName("2001:db8:dead:beef:abcd:abcc:ffff:ffff"); + + Subnet mask = new Subnet(a, 96); + + assertTrue(mask.inSubnet(a)); + assertTrue(mask.inSubnet(b)); + assertTrue(mask.inSubnet(c)); + assertFalse(mask.inSubnet(d)); + assertFalse(mask.inSubnet(e)); + } + + @Test + public void testSingleIp() throws UnknownHostException { + InetAddress a = InetAddress.getByName("2001:db8:dead:beef:f0ca:cc1a:ac1d:ba5e"); + InetAddress b = InetAddress.getByName("2001:db8::"); + InetAddress c = InetAddress.getByName("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"); + InetAddress d = InetAddress.getByName("2001:db8:dead:beef:f0ca:cc1a:ac1d:ba5f"); + InetAddress e = InetAddress.getByName("2001:db8:dead:beef:f0ca:cc1a:ac1d:ba5d"); + + Subnet mask = new Subnet(a, 128); + + assertTrue(mask.inSubnet(a)); + assertFalse(mask.inSubnet(b)); + assertFalse(mask.inSubnet(c)); + assertFalse(mask.inSubnet(d)); + assertFalse(mask.inSubnet(e)); + } + + @Test + public void testToString() throws UnknownHostException { + InetAddress a = InetAddress.getByName("2001:db8::"); + Subnet mask = new Subnet(a, 32); + + assertEquals("2001:db8:0:0:0:0:0:0/32", mask.toString()); + } + + @Test + public void testEquals() throws UnknownHostException { + Subnet a = new Subnet(InetAddress.getByName("2001:db8::"), 32); + Subnet b = new Subnet(InetAddress.getByName("2001:db8::"), 32); + Subnet c = new Subnet(InetAddress.getByName("2001:db8:dead:beef::"), 64); + Subnet d = new Subnet(InetAddress.getByName("2001:db8:dead:beef::"), 64); - new Subnet(a, 24); + assertTrue(a.equals(b)); + assertFalse(a.equals(c)); + assertFalse(a.equals(d)); + assertFalse(a.equals(null)); } }