Add IpFilter for restricting access to resources from those coming from without (or outside) specific IP ranges.
Add IpAddressMatcher taken from Spring Security used for range tests

IpFilter fixes based on code review comments

Add ip to DefaultFilter
diff --git a/NOTICE b/NOTICE
index afda186..9d26a95 100644
--- a/NOTICE
+++ b/NOTICE
@@ -9,7 +9,7 @@
 available at http://www.javaspecialists.eu/archive/Issue015.html,
 with continued modifications.  
 
-Certain parts (StringUtils etc.) of the source code for this 
-product was copied for simplicity and to reduce dependencies 
-from the source code developed by the Spring Framework Project 
-(http://www.springframework.org).
+Certain parts (StringUtils, IpAddressMatcher, etc.) of the source
+code for this  product was copied for simplicity and to reduce
+dependencies  from the source code developed by the Spring Framework
+Project  (http://www.springframework.org).
diff --git a/support/guice/src/main/java/org/apache/shiro/guice/web/ShiroWebModule.java b/support/guice/src/main/java/org/apache/shiro/guice/web/ShiroWebModule.java
index df95665..0bda765 100644
--- a/support/guice/src/main/java/org/apache/shiro/guice/web/ShiroWebModule.java
+++ b/support/guice/src/main/java/org/apache/shiro/guice/web/ShiroWebModule.java
@@ -38,6 +38,7 @@
 import org.apache.shiro.web.filter.authc.LogoutFilter;

 import org.apache.shiro.web.filter.authc.UserFilter;

 import org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter;

+import org.apache.shiro.web.filter.authz.IpFilter;

 import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;

 import org.apache.shiro.web.filter.authz.PortFilter;

 import org.apache.shiro.web.filter.authz.RolesAuthorizationFilter;

@@ -86,6 +87,8 @@
     @SuppressWarnings({"UnusedDeclaration"})

     public static final Key<SslFilter> SSL = Key.get(SslFilter.class);

     @SuppressWarnings({"UnusedDeclaration"})

+    public static final Key<IpFilter> IP = Key.get(IpFilter.class);

+    @SuppressWarnings({"UnusedDeclaration"})

     public static final Key<UserFilter> USER = Key.get(UserFilter.class);

 

 

diff --git a/web/src/main/java/org/apache/shiro/web/filter/authz/IpAddressMatcher.java b/web/src/main/java/org/apache/shiro/web/filter/authz/IpAddressMatcher.java
new file mode 100644
index 0000000..107c80e
--- /dev/null
+++ b/web/src/main/java/org/apache/shiro/web/filter/authz/IpAddressMatcher.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2002-2016 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
+ *
+ *      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.shiro.web.filter.authz;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+
+/**
+ * Matches a request based on IP Address or subnet mask matching against the remote
+ * address.
+ * <p>
+ * Both IPv6 and IPv4 addresses are supported, but a matcher which is configured with an
+ * IPv4 address will never match a request which returns an IPv6 address, and vice-versa.
+ *
+ * @see <a href="https://github.com/spring-projects/spring-security/blob/master/web/src/main/java/org/springframework/security/web/util/matcher/IpAddressMatcher.java">Original Spring Security version</a>
+ * @since 2.0 
+ */
+public final class IpAddressMatcher {
+    private final int nMaskBits;
+    private final InetAddress requiredAddress;
+   
+    /**
+     * Takes a specific IP address or a range specified using the IP/Netmask (e.g.
+     * 192.168.1.0/24 or 202.24.0.0/14).
+     *
+     * @param ipAddress the address or range of addresses from which the request must
+     * come.
+     */
+    public IpAddressMatcher(String ipAddress) {
+        int i = ipAddress.indexOf('/');
+        if (i > 0) {
+            nMaskBits = Integer.parseInt(ipAddress.substring(i + 1));
+            ipAddress = ipAddress.substring(0, i);
+        } else {
+            nMaskBits = -1;
+        }
+        requiredAddress = parseAddress(ipAddress);
+    }
+
+    public boolean matches(String address) {
+        InetAddress remoteAddress = parseAddress(address);
+
+        if (!requiredAddress.getClass().equals(remoteAddress.getClass())) {
+            return false;
+        }
+
+        if (nMaskBits < 0) {
+            return remoteAddress.equals(requiredAddress);
+        }
+
+        byte[] remAddr = remoteAddress.getAddress();
+        byte[] reqAddr = requiredAddress.getAddress();
+
+        int oddBits = nMaskBits % 8;
+        int nMaskBytes = nMaskBits / 8 + (oddBits == 0 ? 0 : 1);
+        byte[] mask = new byte[nMaskBytes];
+
+        Arrays.fill(mask, 0, oddBits == 0 ? mask.length : mask.length - 1, (byte) 0xFF);
+
+        if (oddBits != 0) {
+            int finalByte = (1 << oddBits) - 1;
+            finalByte <<= 8 - oddBits;
+            mask[mask.length - 1] = (byte) finalByte;
+        }
+
+        for (int i = 0; i < mask.length; i++) {
+            if ((remAddr[i] & mask[i]) != (reqAddr[i] & mask[i])) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private InetAddress parseAddress(String address) {
+        try {
+            return InetAddress.getByName(address);
+        }
+        catch (UnknownHostException e) {
+            throw new IllegalArgumentException("Failed to parse address" + address, e);
+        }
+    }
+}
diff --git a/web/src/main/java/org/apache/shiro/web/filter/authz/IpFilter.java b/web/src/main/java/org/apache/shiro/web/filter/authz/IpFilter.java
new file mode 100644
index 0000000..c5bd4a9
--- /dev/null
+++ b/web/src/main/java/org/apache/shiro/web/filter/authz/IpFilter.java
@@ -0,0 +1,142 @@
+/*
+ * 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.shiro.web.filter.authz;
+
+import org.apache.shiro.config.ConfigurationException;
+import org.apache.shiro.util.StringUtils;
+import org.apache.shiro.web.util.WebUtils;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Collection;
+
+/**
+ * A Filter that requires the request to be from within a specific set of IP
+ * address ranges and / or not from with a specific (denied) set.
+ * <p/>
+ * Example config:
+ * <pre>
+ * [main]
+ * localLan = org.apache.shiro.web.filter.authz.IpFilter
+ * localLan.authorizedIps = 192.168.10.0/24
+ * localLan.deniedIps = 192.168.10.10/32
+ * <p/>
+ * [urls]
+ * /some/path/** = localLan
+ * # override for just this path:
+ * /another/path/** = localLan
+ * </pre>
+ *
+ * @since 2.0 
+ */
+public class IpFilter extends AuthorizationFilter {
+
+    private static IpSource DEFAULT_IP_SOURCE = new IpSource() {
+            public Collection<String> getAuthorizedIps() {
+                return Collections.emptySet();
+            }
+            public Collection<String> getDeniedIps() {
+                return Collections.emptySet();
+            }
+        };
+    
+    private IpSource ipSource = DEFAULT_IP_SOURCE;
+
+    private List<IpAddressMatcher> authorizedIpMatchers = Collections.emptyList();
+    private List<IpAddressMatcher> deniedIpMatchers = Collections.emptyList();
+
+    /**
+     * Specifies a set of (comma, tab or space-separated) strings representing
+     * IP address representing IPv4 or IPv6 ranges / CIDRs from which access
+     * should be allowed (if the IP is not included in either the list of
+     * statically defined denied IPs or the dynamic list of IPs obtained from
+     * the IP source.
+     */
+    public void setAuthorizedIps(String authorizedIps) {
+        String[] ips = StringUtils.tokenizeToStringArray(authorizedIps, ", \t");
+        if (ips != null && ips.length > 0) {
+            authorizedIpMatchers = new ArrayList<IpAddressMatcher>();
+            for (String ip : ips) {
+                authorizedIpMatchers.add(new IpAddressMatcher(ip));
+            }
+        }
+    }
+
+    /**
+     * Specified a set of (comma, tab or space-separated) strings representing
+     * IP address representing IPv4 or IPv6 ranges / CIDRs from which access
+     * should be blocked.
+     */
+    public void setDeniedIps(String deniedIps) {
+        String[] ips = StringUtils.tokenizeToStringArray(deniedIps, ", \t");
+        if (ips != null && ips.length > 0) {
+            deniedIpMatchers = new ArrayList<IpAddressMatcher>();
+            for (String ip : ips) {
+                deniedIpMatchers.add(new IpAddressMatcher(ip));
+            }
+        }
+    }
+
+    public void setIpSource(IpSource source) {
+        this.ipSource = source;
+    }
+
+    /**
+     * Returns the remote host for a given HTTP request. By default uses the
+     * remote method ServletRequest.getRemoteAddr(). May be overriden by
+     * subclasses to obtain address information from specific headers (e.g. XFF
+     * or Forwarded) in situations with reverse proxies.
+     */
+    public String getHostFromRequest(ServletRequest request) {
+        return request.getRemoteAddr();
+    }
+
+    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
+        String remoteIp = getHostFromRequest(request);
+        for (IpAddressMatcher matcher : deniedIpMatchers) {
+            if (matcher.matches(remoteIp)) {
+                return false;
+            }
+        }
+        for (String ip : ipSource.getDeniedIps()) {
+            IpAddressMatcher matcher = new IpAddressMatcher(ip);
+            if (matcher.matches(remoteIp)) {
+                return false;
+            }
+        }
+        for (IpAddressMatcher matcher : authorizedIpMatchers) {
+            if (matcher.matches(remoteIp)) {
+                return true;
+            }
+        }
+        for (String ip : ipSource.getAuthorizedIps()) {
+            IpAddressMatcher matcher = new IpAddressMatcher(ip);
+            if (matcher.matches(remoteIp)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/web/src/main/java/org/apache/shiro/web/filter/authz/IpSource.java b/web/src/main/java/org/apache/shiro/web/filter/authz/IpSource.java
new file mode 100644
index 0000000..7b2f626
--- /dev/null
+++ b/web/src/main/java/org/apache/shiro/web/filter/authz/IpSource.java
@@ -0,0 +1,43 @@
+/*
+ * 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.shiro.web.filter.authz;
+
+import java.util.Collection;
+
+/**
+ * Represents a source of information for IP restrictions (see IpFilter)
+ * @since 2.0 
+ */
+public interface IpSource {
+
+    /**
+     * Returns a set of strings representing IP address representing
+     * IPv4 or IPv6 ranges / CIDRs. e.g. 192.168.0.0/16 from which
+     * access should be allowed (if and only if the IP is not included
+     * in the list of denied IPs)
+     */
+    public Collection<String> getAuthorizedIps();
+
+    /**
+     * Returns a set of strings representing IP address representing
+     * IPv4 or IPv6 ranges / CIDRs. e.g. 192.168.0.0/16 from which
+     * access should be denied.
+     */
+    public Collection<String> getDeniedIps();
+}
diff --git a/web/src/main/java/org/apache/shiro/web/filter/mgt/DefaultFilter.java b/web/src/main/java/org/apache/shiro/web/filter/mgt/DefaultFilter.java
index 036f62f..a023feb 100644
--- a/web/src/main/java/org/apache/shiro/web/filter/mgt/DefaultFilter.java
+++ b/web/src/main/java/org/apache/shiro/web/filter/mgt/DefaultFilter.java
@@ -41,6 +41,7 @@
     authc(FormAuthenticationFilter.class),
     authcBasic(BasicHttpAuthenticationFilter.class),
     authcBearer(BearerHttpAuthenticationFilter.class),
+    ip(IpFilter.class),
     logout(LogoutFilter.class),
     noSessionCreation(NoSessionCreationFilter.class),
     perms(PermissionsAuthorizationFilter.class),
diff --git a/web/src/test/java/org/apache/shiro/web/filter/authz/IpAddressMatcherTests.java b/web/src/test/java/org/apache/shiro/web/filter/authz/IpAddressMatcherTests.java
new file mode 100644
index 0000000..ad87303
--- /dev/null
+++ b/web/src/test/java/org/apache/shiro/web/filter/authz/IpAddressMatcherTests.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2002-2016 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
+ *
+ *      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.shiro.web.filter.authz;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
+import org.junit.Test;
+
+/**
+ * @since 2.0 
+ */
+public class IpAddressMatcherTests {
+    final IpAddressMatcher v6matcher = new IpAddressMatcher("fe80::21f:5bff:fe33:bd68");
+    final IpAddressMatcher v4matcher = new IpAddressMatcher("192.168.1.104");
+    final String ipv6Address = "fe80::21f:5bff:fe33:bd68";
+    final String ipv4Address = "192.168.1.104";
+
+    @Test
+    public void ipv6MatcherMatchesIpv6Address() {
+        assertTrue(v6matcher.matches(ipv6Address));
+    }
+    
+    @Test
+    public void ipv6MatcherDoesntMatchIpv4Address() {
+        assertFalse(v6matcher.matches(ipv4Address));
+    }
+    
+    @Test
+    public void ipv4MatcherMatchesIpv4Address() {
+        assertTrue(v4matcher.matches(ipv4Address));
+    }
+    
+    @Test
+    public void ipv4SubnetMatchesCorrectly() throws Exception {
+        IpAddressMatcher matcher = new IpAddressMatcher("192.168.1.0/24");
+        assertTrue(matcher.matches(ipv4Address));
+        matcher = new IpAddressMatcher("192.168.1.128/25");
+        assertFalse(matcher.matches(ipv4Address));
+        assertTrue(matcher.matches("192.168.1.159"));
+    }
+    
+    @Test
+    public void ipv6RangeMatches() throws Exception {
+        IpAddressMatcher matcher = new IpAddressMatcher("2001:DB8::/48");
+        assertTrue(matcher.matches("2001:DB8:0:0:0:0:0:0"));
+        assertTrue(matcher.matches("2001:DB8:0:0:0:0:0:1"));
+        assertTrue(matcher.matches("2001:DB8:0:FFFF:FFFF:FFFF:FFFF:FFFF"));
+        assertFalse(matcher.matches("2001:DB8:1:0:0:0:0:0"));
+    }
+    
+    // https://github.com/spring-projects/spring-security/issues/1970q
+    @Test
+    public void zeroMaskMatchesAnything() throws Exception {
+        IpAddressMatcher matcher = new IpAddressMatcher("0.0.0.0/0");
+        
+        assertTrue(matcher.matches("123.4.5.6"));
+        assertTrue(matcher.matches("192.168.0.159"));
+        
+        matcher = new IpAddressMatcher("192.168.0.159/0");
+        assertTrue(matcher.matches("123.4.5.6"));
+        assertTrue(matcher.matches("192.168.0.159"));
+    }
+}
diff --git a/web/src/test/java/org/apache/shiro/web/filter/authz/IpFilterTest.java b/web/src/test/java/org/apache/shiro/web/filter/authz/IpFilterTest.java
new file mode 100644
index 0000000..c5def24
--- /dev/null
+++ b/web/src/test/java/org/apache/shiro/web/filter/authz/IpFilterTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.shiro.web.filter.authz;
+
+import org.junit.Test;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import static org.easymock.EasyMock.*;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
+/**
+ * Test cases for the {@link AuthorizationFilter} class.
+ * @since 2.0 
+ */
+public class IpFilterTest {
+
+    @Test
+    public void accessShouldBeDeniedByDefault() throws Exception {
+        IpFilter filter = new IpFilter();
+        HttpServletRequest request = createNiceMock(HttpServletRequest.class);
+        expect(request.getRemoteAddr()).andReturn("192.168.42.42");
+        replay(request);
+        assertFalse(filter.isAccessAllowed(request, null, null));
+        verify(request);
+    }
+
+    @Test
+    public void accessShouldBeDeniedWhenNotInTheAllowedSet() throws Exception {
+        IpFilter filter = new IpFilter();
+        filter.setAuthorizedIps("192.168.33/24");
+        HttpServletRequest request = createNiceMock(HttpServletRequest.class);
+        expect(request.getRemoteAddr()).andReturn("192.168.42.42");
+        replay(request);
+        assertFalse(filter.isAccessAllowed(request, null, null));
+        verify(request);
+    }
+
+    @Test
+    public void accessShouldBeGrantedToIpsInTheAllowedSet() throws Exception {
+        IpFilter filter = new IpFilter();
+        filter.setAuthorizedIps("192.168.32/24 192.168.33/24 192.168.34/24");
+        HttpServletRequest request = createNiceMock(HttpServletRequest.class);
+        expect(request.getRemoteAddr()).andReturn("192.168.33.44");
+        replay(request);
+        assertFalse(filter.isAccessAllowed(request, null, null));
+        verify(request);
+    }
+
+    @Test
+    public void deniedTakesPrecedenceOverAllowed() throws Exception {
+        IpFilter filter = new IpFilter();
+        filter.setAuthorizedIps("192.168.0.0/16");
+        filter.setDeniedIps("192.168.33.0/24");
+        HttpServletRequest request = createNiceMock(HttpServletRequest.class);
+        expect(request.getRemoteAddr()).andReturn("192.168.33.44");
+        replay(request);
+        assertFalse(filter.isAccessAllowed(request, null, null));
+        verify(request);
+    }
+
+    @Test
+    public void willBlockAndAllowBasedOnIpSource() throws Exception {
+        IpSource source = new IpSource() {
+                public Collection<String> getAuthorizedIps() {
+                    return Collections.singleton("192.168.0.0/16");
+                }
+                public Collection<String> getDeniedIps() {
+                    return Collections.singleton("192.168.33.0/24");
+                }
+            };
+        IpFilter filter = new IpFilter();
+        filter.setIpSource(source);
+        HttpServletRequest request = createNiceMock(HttpServletRequest.class);
+        expect(request.getRemoteAddr()).andReturn("192.168.33.44");
+        replay(request);
+        assertFalse(filter.isAccessAllowed(request, null, null));
+        verify(request);
+    }
+}