mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-18 15:33:09 +00:00
Extend IP authentication over IPv6 addresses. Begin adding unit tests for authentication.
git-svn-id: http://scm.dspace.org/svn/repo/dspace/trunk@6024 9c30dcfa-912a-0410-8fc2-9e0234be79fd
This commit is contained in:
@@ -7,20 +7,25 @@
|
||||
*/
|
||||
package org.dspace.authenticate;
|
||||
|
||||
import java.net.Inet6Address;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
* Quickly tests whether a given IPv4 4-byte address matches an IP range. An
|
||||
* Quickly tests whether a given IP address matches an IP range. An
|
||||
* {@code IPMatcher} is initialized with a particular IP range specification.
|
||||
* Calls to {@link IPMatcher#match(String) match} method will then quickly
|
||||
* determine whether a given IP falls within that range.
|
||||
* <p>
|
||||
* Supported range specifications areL
|
||||
* Supported range specifications are:
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>Full IP address, e.g. {@code 12.34.56.78}</li>
|
||||
* <li>Partial IP address, e.g. {@code 12.34} (which matches any IP starting
|
||||
* <li>Full IPv4 address, e.g. {@code 12.34.56.78}</li>
|
||||
* <li>Full IPv6 address, e.g. {@code 2001:18e8:3:171:218:8bff:fe2a:56a4}</li>
|
||||
* <li>Partial IPv4 address, e.g. {@code 12.34} (which matches any IP starting
|
||||
* {@code 12.34})</li>
|
||||
* <li>Network/netmask, e.g. {@code 18.25.0.0/255.255.0.0}</li>
|
||||
* <li>CIDR slash notation, e.g. {@code 18.25.0.0/16}</li>
|
||||
* <li>IPv4 network/netmask, e.g. {@code 18.25.0.0/255.255.0.0}</li>
|
||||
* <li>IPv4 or IPv6 CIDR slash notation, e.g. {@code 18.25.0.0/16},
|
||||
* {@code 2001:18e8:3:171::/64}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @version $Revision$
|
||||
@@ -38,7 +43,7 @@ public class IPMatcher
|
||||
* Construct an IPMatcher that will test for the given IP specification
|
||||
*
|
||||
* @param ipSpec
|
||||
* IP specification (full or partial URL, network/netmask,
|
||||
* IP specification (full or partial address, network/netmask,
|
||||
* network/cidr)
|
||||
* @throws IPMatcherException
|
||||
* if there is an error parsing the specification (i.e. it is
|
||||
@@ -47,15 +52,60 @@ public class IPMatcher
|
||||
public IPMatcher(String ipSpec) throws IPMatcherException
|
||||
{
|
||||
// Boil all specs down to network + mask
|
||||
network = new byte[4];
|
||||
netmask = new byte[] { -1, -1, -1, -1 };
|
||||
|
||||
// Allow partial IP
|
||||
boolean mustHave4 = false;
|
||||
|
||||
String ipPart = ipSpec;
|
||||
String[] parts = ipSpec.split("/");
|
||||
|
||||
if (parts[0].indexOf(':') >= 0)
|
||||
{ // looks like IPv6
|
||||
try
|
||||
{
|
||||
network = Inet6Address.getByName(parts[0]).getAddress();
|
||||
}
|
||||
catch (UnknownHostException e)
|
||||
{
|
||||
throw new IPMatcherException(
|
||||
"Malformed IP range specification " + ipSpec, e);
|
||||
}
|
||||
|
||||
netmask = new byte[16];
|
||||
switch(parts.length)
|
||||
{
|
||||
case 2: // CIDR notation: calculate the mask
|
||||
int maskBits;
|
||||
try {
|
||||
maskBits = Integer.parseInt(parts[1]);
|
||||
} catch (NumberFormatException nfe) {
|
||||
throw new IPMatcherException(
|
||||
"Malformed IP range specification " + ipSpec, nfe);
|
||||
}
|
||||
if (maskBits < 0 || maskBits > 128)
|
||||
throw new IPMatcherException("Mask bits out of range 0-128 "
|
||||
+ ipSpec);
|
||||
|
||||
int maskBytes = maskBits/8;
|
||||
for (int i = 0; i < maskBytes; i++)
|
||||
netmask[i] = (byte) 0Xff;
|
||||
netmask[maskBytes] = (byte) ((byte) 0Xff << 8-(maskBits % 8)); // FIXME test!
|
||||
for (int i = maskBytes+1; i < (128/8); i++)
|
||||
netmask[i] = 0;
|
||||
break;
|
||||
case 1: // No explicit mask: fill the mask with 1s
|
||||
for (int i = 0; i < netmask.length; i++)
|
||||
netmask[i] = (byte) 0Xff;
|
||||
break;
|
||||
default:
|
||||
throw new IPMatcherException("Malformed IP range specification "
|
||||
+ ipSpec);
|
||||
}
|
||||
}
|
||||
else
|
||||
{ // assume IPv4
|
||||
// Allow partial IP
|
||||
boolean mustHave4 = false;
|
||||
|
||||
network = new byte[4];
|
||||
netmask = new byte[4];
|
||||
switch (parts.length)
|
||||
{
|
||||
case 2:
|
||||
@@ -98,6 +148,8 @@ public class IPMatcher
|
||||
|
||||
case 1:
|
||||
// Get IP
|
||||
for (int i = 0; i < netmask.length; i++)
|
||||
netmask[i] = -1;
|
||||
int partCount = ipToBytes(ipPart, network, mustHave4);
|
||||
|
||||
// If partial IP, set mask for remaining bytes
|
||||
@@ -112,14 +164,17 @@ public class IPMatcher
|
||||
throw new IPMatcherException("Malformed IP range specification "
|
||||
+ ipSpec);
|
||||
}
|
||||
network = ip4ToIp6(network);
|
||||
netmask = ip4MaskToIp6(netmask);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill out a given four-byte array with the IP address specified in the
|
||||
* Fill out a given four-byte array with the IPv4 address specified in the
|
||||
* given String
|
||||
*
|
||||
* @param ip
|
||||
* IP address as a dot-delimited String
|
||||
* IPv4 address as a dot-delimited String
|
||||
* @param bytes
|
||||
* 4-byte array to fill out
|
||||
* @param mustHave4
|
||||
@@ -132,7 +187,7 @@ public class IPMatcher
|
||||
* outside of range 0-255, too many numbers, less than 4 numbers
|
||||
* if {@code mustHave4} is true
|
||||
*/
|
||||
private int ipToBytes(String ip, byte[] bytes, boolean mustHave4)
|
||||
private static int ipToBytes(String ip, byte[] bytes, boolean mustHave4)
|
||||
throws IPMatcherException
|
||||
{
|
||||
String[] parts = ip.split("\\.");
|
||||
@@ -181,13 +236,27 @@ public class IPMatcher
|
||||
*/
|
||||
public boolean match(String ipIn) throws IPMatcherException
|
||||
{
|
||||
byte[] bytes = new byte[4];
|
||||
byte[] candidate;
|
||||
|
||||
ipToBytes(ipIn, bytes, true);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
if (ipIn.indexOf(':') < 0)
|
||||
{
|
||||
if ((bytes[i] & netmask[i]) != (network[i] & netmask[i]))
|
||||
candidate = new byte[4];
|
||||
ipToBytes(ipIn, candidate, true);
|
||||
candidate = ip4ToIp6(candidate);
|
||||
}
|
||||
else
|
||||
try
|
||||
{
|
||||
candidate = Inet6Address.getByName(ipIn).getAddress();
|
||||
}
|
||||
catch (UnknownHostException e)
|
||||
{
|
||||
throw new IPMatcherException("Malformed IPv6 address ",e);
|
||||
}
|
||||
|
||||
for (int i = 0; i < netmask.length; i++)
|
||||
{
|
||||
if ((candidate[i] & netmask[i]) != (network[i] & netmask[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -195,4 +264,42 @@ public class IPMatcher
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an IPv4 address to an IPv6 IPv4-compatible address.
|
||||
* @param ip4 an IPv4 address
|
||||
* @return the corresponding IPv6 address
|
||||
* @throws IllegalArgumentException if ip4 is not exactly four octets long.
|
||||
*/
|
||||
private static byte[] ip4ToIp6(byte[] ip4)
|
||||
{
|
||||
if (ip4.length != 4)
|
||||
throw new IllegalArgumentException("IPv4 address must be four octets");
|
||||
|
||||
byte[] ip6 = new byte[16];
|
||||
for (int i = 0; i < 16-4; i++)
|
||||
ip6[i] = 0;
|
||||
for (int i = 0; i < 4; i++)
|
||||
ip6[12+i] = ip4[i];
|
||||
return ip6;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an IPv4 mask to the equivalent IPv6 mask.
|
||||
* @param ip4 an IPv4 mask
|
||||
* @return the corresponding IPv6 mask
|
||||
* @throws IllegalArgumentException if ip4 is not exactly four octets long.
|
||||
*/
|
||||
private static byte[] ip4MaskToIp6(byte[] ip4)
|
||||
{
|
||||
if (ip4.length != 4)
|
||||
throw new IllegalArgumentException("IPv4 mask must be four octets");
|
||||
|
||||
byte[] ip6 = new byte[16];
|
||||
for (int i = 0; i < 16-4; i++)
|
||||
ip6[i] = (byte) 0Xff;
|
||||
for (int i = 0; i < 4; i++)
|
||||
ip6[12+i] = ip4[i];
|
||||
return ip6;
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
*
|
||||
*/
|
||||
package org.dspace.authenticate;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.dspace.AbstractUnitTest;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* @author Mark Wood
|
||||
*/
|
||||
public class IPMatcherTest extends AbstractUnitTest
|
||||
{
|
||||
private static final String IP6_FULL_ADDRESS1 = "2001:18e8:3:171:218:8bff:fe2a:56a4";
|
||||
private static final String IP6_FULL_ADDRESS2 = "2001:18e8:3:171:218:8bff:fe2a:56a3";
|
||||
private static final String IP6_MASKED_ADDRESS = "2001:18e8:3::/48";
|
||||
|
||||
private static IPMatcher ip6FullMatcher;
|
||||
private static IPMatcher ip6MaskedMatcher;
|
||||
|
||||
/**
|
||||
* This also tests instantiation of correct masked and unmasked IPv6 addresses.
|
||||
* @throws IPMatcherException
|
||||
*/
|
||||
@BeforeClass
|
||||
static public void setUp() throws IPMatcherException
|
||||
{
|
||||
ip6FullMatcher = new IPMatcher(IP6_FULL_ADDRESS1);
|
||||
ip6MaskedMatcher = new IPMatcher(IP6_MASKED_ADDRESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method for {@link org.dspace.authenticate.IPMatcher#IPMatcher(java.lang.String)}.
|
||||
*/
|
||||
@Test(expected=IPMatcherException.class)
|
||||
public void testIPMatcherIp6Incomplete()
|
||||
throws IPMatcherException
|
||||
{
|
||||
new IPMatcher("1234:5"); // Incomplete IPv6 address
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method for {@link org.dspace.authenticate.IPMatcher#IPMatcher(java.lang.String)}.
|
||||
*/
|
||||
@Test(expected=IPMatcherException.class)
|
||||
public void testIPMatcherIp6MaskOutOfRange()
|
||||
throws IPMatcherException
|
||||
{
|
||||
new IPMatcher("123::456/999"); // Mask bits out of range
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method for {@link org.dspace.authenticate.IPMatcher#IPMatcher(java.lang.String)}.
|
||||
*/
|
||||
@Test(expected=IPMatcherException.class)
|
||||
public void testIPMatcherIp6MaskNotNumeric()
|
||||
throws IPMatcherException
|
||||
{
|
||||
new IPMatcher("123::456/abc"); // Mask is not a number
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method for {@link org.dspace.authenticate.IPMatcher#IPMatcher(java.lang.String)}.
|
||||
*/
|
||||
@Test(expected=IPMatcherException.class)
|
||||
public void testIPMatcherIp6TooManySlashes()
|
||||
throws IPMatcherException
|
||||
{
|
||||
new IPMatcher("123::456/12/12"); // Too many slashes
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method for
|
||||
* {@link org.dspace.authenticate.IPMatcher#match(java.lang.String)}.
|
||||
*/
|
||||
@Test
|
||||
public void testIp6FullMatch()
|
||||
throws IPMatcherException
|
||||
{
|
||||
assertTrue("IPv6 full match fails", ip6FullMatcher
|
||||
.match(IP6_FULL_ADDRESS1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method for
|
||||
* {@link org.dspace.authenticate.IPMatcher#match(java.lang.String)}.
|
||||
*/
|
||||
@Test
|
||||
public void testIp6MisMatch()
|
||||
throws IPMatcherException
|
||||
{
|
||||
assertFalse("IPv6 full nonmatch succeeds", ip6FullMatcher
|
||||
.match(IP6_FULL_ADDRESS2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method for
|
||||
* {@link org.dspace.authenticate.IPMatcher#match(java.lang.String)}.
|
||||
*/
|
||||
@Test
|
||||
public void testIp6MaskedMatch()
|
||||
throws IPMatcherException
|
||||
{
|
||||
assertTrue("IPv6 masked match fails", ip6MaskedMatcher
|
||||
.match(IP6_FULL_ADDRESS2));
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
static public void cleanup()
|
||||
{
|
||||
ip6FullMatcher = null;
|
||||
ip6MaskedMatcher = null;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user