| IPFilter.java |
/*
* $Id: IPFilter.java,v 1.48 2013/01/29 10:48:17 agoubard Exp $
*
* Copyright 2003-2010 Online Breedband B.V.
* See the COPYRIGHT file for redistribution and use restrictions.
*/
package org.xins.server;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.BitSet;
import java.util.regex.Pattern;
import org.xins.common.MandatoryArgumentChecker;
import org.xins.common.text.ParseException;
/**
* Filter for IP addresses.
*
* <a name="format"></a>
* <h3>Filter expression format</h3>
*
* <p>An <code>IPFilter</code> instance is created using a so-called
* <em>filter expression</em>. This filter expression specifies the IP address
* and mask to use for matching a subject IP address.
*
* <p>IPv4 filters will only accept IPv4 addresses and IPv6 filters will only
* accept IPv6 addresses.
*
* <h3>Example code</h3>
*
* <p>An <code>IPFilter</code> object is
* created and used as follows:
*
* <blockquote><code>IPFilter filter = IPFilter.parseFilter("10.0.0.0/24");
* <br>if (filter.match("10.0.0.1")) {
* <br> // IP is granted access
* <br>} else {
* <br> // IP is denied access
* <br>}</code></blockquote>
*
* <blockquote><code>IPFilter filter = IPFilter.parseFilter("3FFE:200::/32");
* <br>if (filter.match("1fff:0:a88:85a3::ac1f:8001")) {
* <br> // IP is granted access
* <br>} else {
* <br> // IP is denied access
* <br>}</code></blockquote>
*
* @version $Revision: 1.48 $ $Date: 2013/01/29 10:48:17 $
* @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
* @author Peter Troon
* @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
*
* @since XINS 1.0.0
*/
public final class IPFilter {
/**
* A basic IPv4 pattern.
*/
private static final Pattern IP_4_PATTERN = Pattern.compile("^\\d{1,3}(\\.\\d{1,3}){3}$");
/**
* The character that delimits the IP address and the mask of the provided
* filter.
*/
private static final char IP_MASK_DELIMETER = '/';
/**
* The expression of this filter, cannot be <code>null</code>.
*/
private final String _expression;
/**
* The base IP address, as a <code>String</code>. Never <code>null</code>.
*/
private final String _baseIPString;
/**
* The base IP address.
*/
private final BitSet _baseIP;
/**
* The mask of this filter. Can only have a value between 0 and 128.
*/
private final int _mask;
/**
* <code>true</code> only if the filter is for IPv6 addresses.
*/
private final boolean _isIPv6Filter;
/**
* Creates an <code>IPFilter</code> object for the specified filter
* expression. The expression consists of a base IP address and a bit
* count. The bit count indicates how many bits in an IP address must match
* the bits in the base IP address.
*
* @param ipString
* the base IP address, as a character string, should not be
* <code>null</code>.
*
* @param baseIP
* the base IP address, as a series of bits, should not be
* <code>null</code>.
*
* @param mask
* the mask, between 0 and 128 (inclusive).
*/
private IPFilter(String ipString, BitSet baseIP, int mask) {
_expression = ipString + IP_MASK_DELIMETER + mask;
_baseIPString = ipString;
_baseIP = baseIP;
_mask = mask;
_isIPv6Filter = ipString.indexOf(':') != -1;
}
/**
* Creates an <code>IPFilter</code> object for the specified filter
* expression. The expression consists of a base IP address and a bit
* count. The bit count indicates how many bits in an IP address must match
* the bits in the base IP address.
*
* @param expression
* the filter expression, cannot be <code>null</code> and must match
* <a href="#format">the format for a filter expression</a>.
* if no mask is passed 32 is assumed for IPv4 addresses and 128
* for IPv6 addresses.
*
* @return
* the constructed <code>IPFilter</code> object, never
* <code>null</code>.
*
* @throws IllegalArgumentException
* if <code>expression == null</code>.
*
* @throws ParseException
* if <code>expression</code> does not match the specified format.
*/
public static final IPFilter parseIPFilter(String expression)
throws IllegalArgumentException, ParseException {
// Check preconditions
MandatoryArgumentChecker.check("expression", expression);
// Find the slash ('/') character
int slashPosition = expression.indexOf(IP_MASK_DELIMETER);
String ipString;
int mask;
// If we have a slash, then it cannot be at the first or last position
if (slashPosition >= 0) {
if (slashPosition == 0 || slashPosition == expression.length() - 1) {
throw new ParseException("The string \"" + expression + "\" is not a valid IP filter expression.");
}
// Split the IP and the mask
ipString = expression.substring(0, slashPosition);
mask = parseMask(expression.substring(slashPosition + 1));
// If we don't have a slash, then parse the IP address only and assume
// the mask to be 32 bits
} else {
ipString = expression;
if (ipString.indexOf(':') == -1) {
mask = 32;
} else {
mask = 128;
}
}
BitSet ipBase = ipStringToBitSet(ipString);
// Create and return an IPFilter object
return new IPFilter(ipString, ipBase, mask);
}
/**
* Parses the specified mask.
*
* @param maskString
* the mask string, may not be <code>null</code>.
*
* @return
* an integer representing the value of the mask, between 0 and 128.
*
* @throws ParseException
* if the specified string is not a mask between 0 and 128, with no
* leading zeroes.
*/
private static int parseMask(String maskString)
throws ParseException {
// Convert to an int
int mask;
try {
mask = Integer.parseInt(maskString);
// Catch conversion exception
} catch (NumberFormatException nfe) {
throw new ParseException("The mask string \"" + maskString + "\" is not a valid number.");
}
// Number must be between 0 and 32
if (mask < 0 || mask > 128) {
throw new ParseException("The mask string \"" + maskString + "\" is not a number between 0 and 128.");
}
// Disallow a leading zero
if (maskString.length() >= 2 && maskString.charAt(0) == '0') {
throw new ParseException("The mask string \"" + maskString + "\" starts with a leading zero.");
}
return mask;
}
/**
* Returns the filter expression.
*
* @return
* the original filter expression, never <code>null</code>.
*/
public final String getExpression() {
return _expression;
}
/**
* Returns the base IP address.
*
* @return
* the base IP address, in the form
* <code><em>a</em>.<em>a</em>.<em>a</em>.<em>a</em>/<em>n</em></code>,
* where <em>a</em> is a number between 0 and 255, with no leading
* zeroes; never <code>null</code>.
*/
public final String getBaseIP() {
return _baseIPString;
}
/**
* Returns the mask.
*
* @return
* the mask, between 0 and 128.
*/
public final int getMask() {
return _mask;
}
/**
* Determines if the specified IP address is authorized.
*
* <p>Note IPv6 addresses are only accepted by IPv6 ACLs and
* IPv4 addresses are only accepted by IPv4 ACLs.
*
* @param ipString
* the IP address of which must be determined if it is authorized,
* cannot be <code>null</code>.
*
* @return
* <code>true</code> if the IP address is authorized to access the
* protected resource, otherwise <code>false</code>.
*
* @throws IllegalArgumentException
* if <code>ipString == null</code>.
*
* @throws ParseException
* if <code>ip</code> does not match the specified format.
*/
public final boolean match(String ipString)
throws IllegalArgumentException, ParseException {
// Check preconditions
MandatoryArgumentChecker.check("ipString", ipString);
BitSet ipBits = ipStringToBitSet(ipString);
if ((ipString.indexOf(':') == -1 && _isIPv6Filter) ||
(ipString.indexOf(':') != -1 && !_isIPv6Filter)) {
return false;
}
// Short-circuit if mask is 0 bits
if (_mask == 0) {
return true;
}
// Perform the match
ipBits.xor(_baseIP);
int maxLength = ipString.indexOf(':') == -1 ? 32 : 128;
ipBits.clear(0, maxLength - _mask);
boolean match = ipBits.isEmpty();
return match;
}
/**
* Transforms the IP address in a series of bits.
*
* @param ipString
* the String representation of the IP address, cannot be <code>null</code>
* @return
* the series of bits representing the IP address, never <code>null</code>
*
* @throws ParseException
* if the IP address is not a valid IP address.
*/
private static BitSet ipStringToBitSet(String ipString) throws ParseException {
BitSet ipBits = new BitSet();
if (!IP_4_PATTERN.matcher(ipString).matches() && !ipString.contains(":")) {
throw new ParseException("The string \"" + ipString + "\" is not a valid IP address as it does not match the patterns.");
}
byte[] ipBytes;
try {
ipBytes = InetAddress.getByName(ipString).getAddress();
} catch (UnknownHostException ex) {
throw new ParseException("The string \"" + ipString + "\" is not a valid IP address.");
}
if (ipBytes.length != 4 && ipBytes.length != 16) {
throw new ParseException("Incorrect transformation as " + ipBytes.length + " bytes array are created for ip " + ipString);
}
for (int i = 0; i < ipBytes.length * 8; i++) {
boolean isTrue = (ipBytes[ipBytes.length - i / 8 - 1] & (1 << (i % 8))) > 0;
ipBits.set(i, isTrue);
}
return ipBits;
}
/**
* Returns a textual representation of this filter. The implementation of
* this method returns the filter expression passed.
*
* @return
* a textual presentation, never <code>null</code>.
*/
public final String toString() {
return getExpression();
}
}