From 386cc2cb733e4bf23ef4024e954aa2d07d942510 Mon Sep 17 00:00:00 2001 From: Jakob Buchgraber Date: Wed, 5 Mar 2014 00:36:54 +0100 Subject: [PATCH] ipfilter implementation for netty 4/5 [#2129] --- .../ipfilter/AbstractIpFilterHandler.java | 99 +++++++++++ .../handler/ipfilter/IpFilterDecision.java | 28 ++++ .../netty/handler/ipfilter/IpFilterRule.java | 36 ++++ .../handler/ipfilter/IpFilterRuleHandler.java | 62 +++++++ .../handler/ipfilter/IpFilterRuleType.java | 24 +++ .../handler/ipfilter/IpSubnetFilterRule.java | 158 ++++++++++++++++++ .../ipfilter/UniqueIpFilterHandler.java | 59 +++++++ .../netty/handler/ipfilter/package-info.java | 20 +++ .../handler/ipfilter/IpSubnetFilterTest.java | 135 +++++++++++++++ 9 files changed, 621 insertions(+) create mode 100644 handler/src/main/java/io/netty/handler/ipfilter/AbstractIpFilterHandler.java create mode 100644 handler/src/main/java/io/netty/handler/ipfilter/IpFilterDecision.java create mode 100644 handler/src/main/java/io/netty/handler/ipfilter/IpFilterRule.java create mode 100644 handler/src/main/java/io/netty/handler/ipfilter/IpFilterRuleHandler.java create mode 100644 handler/src/main/java/io/netty/handler/ipfilter/IpFilterRuleType.java create mode 100644 handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRule.java create mode 100644 handler/src/main/java/io/netty/handler/ipfilter/UniqueIpFilterHandler.java create mode 100644 handler/src/main/java/io/netty/handler/ipfilter/package-info.java create mode 100644 handler/src/test/java/io/netty/handler/ipfilter/IpSubnetFilterTest.java diff --git a/handler/src/main/java/io/netty/handler/ipfilter/AbstractIpFilterHandler.java b/handler/src/main/java/io/netty/handler/ipfilter/AbstractIpFilterHandler.java new file mode 100644 index 0000000000..92fb0e6f44 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ipfilter/AbstractIpFilterHandler.java @@ -0,0 +1,99 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project 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 io.netty.handler.ipfilter; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.Attribute; +import io.netty.util.AttributeKey; + +import java.net.InetSocketAddress; + +/** + * This class provides the functionality to either accept or reject new {@link Channel}s + * based on their IP address. + *

+ * You should inherit from this class if you would like to implement your own IP-based filter. Basically you have to + * implement {@link #accept(InetSocketAddress)} to decided whether you want to allow or deny a connection from an IP + * address. Furthermore overriding {@link #rejected(ChannelHandlerContext, InetSocketAddress)} gives you the flexibility + * to respond to rejected (denied) connections. If you do not want to send a response, just have it return null. + * Take a look at {@link IpFilterRuleHandler} for details. + */ +public abstract class AbstractIpFilterHandler extends ChannelInboundHandlerAdapter { + private final AttributeKey decisionKey = AttributeKey.valueOf(getClass().getName()); + + @Override + public void channelRegistered(ChannelHandlerContext ctx) throws Exception { + final InetSocketAddress ipAddress = (InetSocketAddress) ctx.channel().remoteAddress(); + + if (!accept(ipAddress)) { + // the channel might be active already + if (ctx.channel().isActive()) { + handleRejected(ctx); + } else { + // if the channel is not active yet, store the decision for later use + // in #channelActive(ChannelHandlerContext ctx) + Attribute decision = ctx.attr(decisionKey); + decision.set(IpFilterDecision.REJECTED); + + super.channelRegistered(ctx); + } + } else { + super.channelRegistered(ctx); + } + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + final IpFilterDecision decision = ctx.attr(decisionKey).get(); + + if (decision == IpFilterDecision.REJECTED) { + handleRejected(ctx); + } else { + super.channelActive(ctx); + } + } + + private void handleRejected(ChannelHandlerContext ctx) { + final InetSocketAddress ipAddress = (InetSocketAddress) ctx.channel().remoteAddress(); + + ChannelFuture rejectedFuture = rejected(ctx, ipAddress); + if (rejectedFuture != null) { + rejectedFuture.addListener(ChannelFutureListener.CLOSE); + } else { + ctx.close(); + } + } + + /** + * This method is called immediately after a {@link io.netty.channel.Channel} gets registered. + * + * @return Return true if connections from this IP address and port should be accepted. False otherwise. + */ + protected abstract boolean accept(InetSocketAddress ipAndPort) throws Exception; + + /** + * This method is called if ipAndPort gets rejected by {@link #accept(InetSocketAddress)}. + * You should override it if you would like to handle (e.g. respond to) rejected addresses. + * + * @return A {@link ChannelFuture} if you perform I/O operations, so that + * the {@link Channel} can be closed once it completes. Null otherwise. + */ + protected abstract ChannelFuture rejected(ChannelHandlerContext ctx, InetSocketAddress ipAndPort); +} diff --git a/handler/src/main/java/io/netty/handler/ipfilter/IpFilterDecision.java b/handler/src/main/java/io/netty/handler/ipfilter/IpFilterDecision.java new file mode 100644 index 0000000000..95e5c5b0c2 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ipfilter/IpFilterDecision.java @@ -0,0 +1,28 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project 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 io.netty.handler.ipfilter; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandler; + +/** + * This enum is used in {@link AbstractIpFilterHandler} to keep the state between the calls to + * {@link ChannelInboundHandler#channelRegistered(ChannelHandlerContext)} and + * {@link ChannelInboundHandler#channelActive(ChannelHandlerContext)}. + */ +public enum IpFilterDecision { + REJECTED +} diff --git a/handler/src/main/java/io/netty/handler/ipfilter/IpFilterRule.java b/handler/src/main/java/io/netty/handler/ipfilter/IpFilterRule.java new file mode 100644 index 0000000000..8a5759ef23 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ipfilter/IpFilterRule.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project 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 io.netty.handler.ipfilter; + +import java.net.InetSocketAddress; + +/** + * Implement this interface to create new rules. + */ +public interface IpFilterRule { + /** + * @return This method should return true if ipAndPort is valid according to your criteria. False otherwise. + */ + boolean matches(InetSocketAddress ipAndPort); + + /** + * @return This method should return {@link IpFilterRuleType#ALLOW} if all + * {@link IpFilterRule#matches(InetSocketAddress)} for which {@link #matches(InetSocketAddress)} + * returns true should the accepted. If you want to exclude all of those IP addresses then + * {@link IpFilterRuleType#DENY} should be returned. + */ + IpFilterRuleType ruleType(); +} diff --git a/handler/src/main/java/io/netty/handler/ipfilter/IpFilterRuleHandler.java b/handler/src/main/java/io/netty/handler/ipfilter/IpFilterRuleHandler.java new file mode 100644 index 0000000000..ded2bcf6f5 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ipfilter/IpFilterRuleHandler.java @@ -0,0 +1,62 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project 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 io.netty.handler.ipfilter; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.Channel; +import java.net.InetSocketAddress; + +/** + * This class allows one to filter new {@link Channel}s based on the + * {@link IpFilterRule}s passed to its constructor. If no rules are provided, all connections + * will be accepted. + * + * If you would like to explicitly take action on rejected {@link Channel}s, you should override + * {@link #rejected(ChannelHandlerContext, InetSocketAddress)}. + */ +@Sharable +public class IpFilterRuleHandler extends AbstractIpFilterHandler { + private final IpFilterRule[] rules; + + public IpFilterRuleHandler(IpFilterRule... rules) { + if (rules == null) { + throw new NullPointerException("rules"); + } + + if (rules.length == 0) { + throw new IllegalArgumentException("You have to provide at least one rule."); + } + + this.rules = rules; + } + + @Override + protected boolean accept(InetSocketAddress ipAndPort) throws Exception { + for (IpFilterRule rule : rules) { + if (rule.matches(ipAndPort)) { + return rule.ruleType() == IpFilterRuleType.ALLOW; + } + } + return true; + } + + @Override + protected ChannelFuture rejected(ChannelHandlerContext ctx, InetSocketAddress ipAndPort) { + return null; + } +} diff --git a/handler/src/main/java/io/netty/handler/ipfilter/IpFilterRuleType.java b/handler/src/main/java/io/netty/handler/ipfilter/IpFilterRuleType.java new file mode 100644 index 0000000000..9d78130388 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ipfilter/IpFilterRuleType.java @@ -0,0 +1,24 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project 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 io.netty.handler.ipfilter; + +/** + * Used in {@link IpFilterRule} to decide if a matching IP Address should be allowed or denied to connect. + */ +public enum IpFilterRuleType { + ALLOW, + DENY +} diff --git a/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRule.java b/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRule.java new file mode 100644 index 0000000000..2eebfdcce7 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRule.java @@ -0,0 +1,158 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project 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 io.netty.handler.ipfilter; + +import java.math.BigInteger; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + +/** + * Use this class to create rules for {@link IpFilterRuleHandler} that group IP addresses into subnets. + * Supports both, IPv4 and IPv6. + */ +public final class IpSubnetFilterRule implements IpFilterRule { + private final IpFilterRule filterRule; + + public IpSubnetFilterRule(String ipAddress, int cidrPrefix, IpFilterRuleType ruleType) { + try { + filterRule = selectFilterRule(InetAddress.getByName(ipAddress), cidrPrefix, ruleType); + } catch (UnknownHostException e) { + throw new IllegalArgumentException("ipAddress", e); + } + } + + public IpSubnetFilterRule(InetAddress ipAddress, int cidrPrefix, IpFilterRuleType ruleType) { + filterRule = selectFilterRule(ipAddress, cidrPrefix, ruleType); + } + + private static IpFilterRule selectFilterRule(InetAddress ipAddress, int cidrPrefix, IpFilterRuleType ruleType) { + if (ipAddress == null) { + throw new NullPointerException("ipAddress"); + } + + if (ruleType == null) { + throw new NullPointerException("ruleType"); + } + + if (ipAddress instanceof Inet4Address) { + return new Ip4SubnetFilterRule((Inet4Address) ipAddress, cidrPrefix, ruleType); + } else if (ipAddress instanceof Inet6Address) { + return new Ip6SubnetFilterRule((Inet6Address) ipAddress, cidrPrefix, ruleType); + } else { + throw new IllegalArgumentException("Only IPv4 and IPv6 addresses are supported"); + } + } + + @Override + public boolean matches(InetSocketAddress ipAndPort) { + return filterRule.matches(ipAndPort); + } + + @Override + public IpFilterRuleType ruleType() { + return filterRule.ruleType(); + } + + private static final class Ip4SubnetFilterRule implements IpFilterRule { + + private final int networkAddress; + private final int subnetMask; + private final IpFilterRuleType ruleType; + + private Ip4SubnetFilterRule(Inet4Address ipAddress, int cidrPrefix, IpFilterRuleType ruleType) { + if (cidrPrefix < 0 || cidrPrefix > 32) { + throw new IllegalArgumentException(String.format("IPv4 requires the subnet prefix to be in range of " + + "[0,32]. The prefix was: %d", cidrPrefix)); + } + + subnetMask = prefixToSubnetMask(cidrPrefix); + networkAddress = ipToInt(ipAddress) & subnetMask; + this.ruleType = ruleType; + } + + @Override + public boolean matches(InetSocketAddress ipAndPort) { + int ipAddress = ipToInt((Inet4Address) ipAndPort.getAddress()); + + return (ipAddress & subnetMask) == networkAddress; + } + + @Override + public IpFilterRuleType ruleType() { + return ruleType; + } + + private static int ipToInt(Inet4Address ipAddress) { + byte[] octets = ipAddress.getAddress(); + assert octets.length == 4; + + return (octets[0] & 0xff) << 24 | + (octets[1] & 0xff) << 16 | + (octets[2] & 0xff) << 8 | + octets[3] & 0xff; + } + + private static int prefixToSubnetMask(int cidrPrefix) { + return -1 << 32 - cidrPrefix; + } + } + + private static final class Ip6SubnetFilterRule implements IpFilterRule { + + private static final BigInteger MINUS_ONE = BigInteger.valueOf(-1); + + private final BigInteger networkAddress; + private final BigInteger subnetMask; + private final IpFilterRuleType ruleType; + + private Ip6SubnetFilterRule(Inet6Address ipAddress, int cidrPrefix, IpFilterRuleType ruleType) { + if (cidrPrefix < 0 || cidrPrefix > 128) { + throw new IllegalArgumentException(String.format("IPv6 requires the subnet prefix to be in range of " + + "[0,128]. The prefix was: %d", cidrPrefix)); + } + + subnetMask = prefixToSubnetMask(cidrPrefix); + networkAddress = ipToInt(ipAddress).and(subnetMask); + this.ruleType = ruleType; + } + + @Override + public boolean matches(InetSocketAddress ipAndPort) { + BigInteger ipAddress = ipToInt((Inet6Address) ipAndPort.getAddress()); + + return ipAddress.and(subnetMask).equals(networkAddress); + } + + @Override + public IpFilterRuleType ruleType() { + return ruleType; + } + + private static BigInteger ipToInt(Inet6Address ipAddress) { + byte[] octets = ipAddress.getAddress(); + assert octets.length == 16; + + return new BigInteger(octets); + } + + private static BigInteger prefixToSubnetMask(int cidrPrefix) { + return MINUS_ONE.shiftLeft(128 - cidrPrefix); + } + } +} diff --git a/handler/src/main/java/io/netty/handler/ipfilter/UniqueIpFilterHandler.java b/handler/src/main/java/io/netty/handler/ipfilter/UniqueIpFilterHandler.java new file mode 100644 index 0000000000..a6dd4487a4 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ipfilter/UniqueIpFilterHandler.java @@ -0,0 +1,59 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project 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 io.netty.handler.ipfilter; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.internal.ConcurrentSet; +import io.netty.channel.Channel; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Set; + +/** + * This class allows one to ensure that at all times for every IP address there is at most one + * {@link Channel} connected to the server. + */ +@ChannelHandler.Sharable +public class UniqueIpFilterHandler extends AbstractIpFilterHandler { + private final Set connected = new ConcurrentSet(); + + @Override + protected boolean accept(InetSocketAddress ipAndPort) throws Exception { + InetAddress ipAddress = ipAndPort.getAddress(); + if (connected.contains(ipAddress)) { + return false; + } else { + connected.add(ipAddress); + return true; + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + InetAddress ipAddress = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress(); + connected.remove(ipAddress); + + super.channelInactive(ctx); + } + + @Override + protected ChannelFuture rejected(ChannelHandlerContext ctx, InetSocketAddress ipAndPort) { + return null; + } +} diff --git a/handler/src/main/java/io/netty/handler/ipfilter/package-info.java b/handler/src/main/java/io/netty/handler/ipfilter/package-info.java new file mode 100644 index 0000000000..c97c430f58 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ipfilter/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project 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 to filter IP addresses (allow/deny). + */ +package io.netty.handler.ipfilter; diff --git a/handler/src/test/java/io/netty/handler/ipfilter/IpSubnetFilterTest.java b/handler/src/test/java/io/netty/handler/ipfilter/IpSubnetFilterTest.java new file mode 100644 index 0000000000..8ca53ad326 --- /dev/null +++ b/handler/src/test/java/io/netty/handler/ipfilter/IpSubnetFilterTest.java @@ -0,0 +1,135 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project 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 io.netty.handler.ipfilter; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.embedded.EmbeddedChannel; +import org.junit.Assert; +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +public class IpSubnetFilterTest { + + @Test + public void testIp4SubnetFilterRule() throws Exception { + IpSubnetFilterRule rule = new IpSubnetFilterRule("192.168.56.1", 24, IpFilterRuleType.ALLOW); + for (int i = 0; i <= 255; i++) { + Assert.assertTrue(rule.matches(newSockAddress(String.format("192.168.56.%d", i)))); + } + Assert.assertFalse(rule.matches(newSockAddress("192.168.57.1"))); + + rule = new IpSubnetFilterRule("91.114.240.1", 23, IpFilterRuleType.ALLOW); + Assert.assertTrue(rule.matches(newSockAddress("91.114.240.43"))); + Assert.assertTrue(rule.matches(newSockAddress("91.114.240.255"))); + Assert.assertTrue(rule.matches(newSockAddress("91.114.241.193"))); + Assert.assertTrue(rule.matches(newSockAddress("91.114.241.254"))); + Assert.assertFalse(rule.matches(newSockAddress("91.115.241.2"))); + } + + @Test + public void testIp6SubnetFilterRule() { + IpSubnetFilterRule rule; + + rule = new IpSubnetFilterRule("2001:db8:abcd:0000::", 52, IpFilterRuleType.ALLOW); + Assert.assertTrue(rule.matches(newSockAddress("2001:db8:abcd:0000::1"))); + Assert.assertTrue(rule.matches(newSockAddress("2001:db8:abcd:0fff:ffff:ffff:ffff:ffff"))); + Assert.assertFalse(rule.matches(newSockAddress("2001:db8:abcd:1000::"))); + } + + @Test + public void testIpFilterRuleHandler() throws Exception { + IpFilterRule filter0 = new IpFilterRule() { + @Override + public boolean matches(InetSocketAddress ipAndPort) { + return "192.168.57.1".equals(ipAndPort.getHostName()); + } + + @Override + public IpFilterRuleType ruleType() { + return IpFilterRuleType.DENY; + } + }; + + IpFilterRuleHandler denyHandler = new IpFilterRuleHandler(filter0) { + private final byte[] message = {1, 2, 3, 4, 5, 6, 7}; + + @Override + protected ChannelFuture rejected(ChannelHandlerContext ctx, InetSocketAddress ipAndPort) { + Assert.assertTrue(ctx.channel().isActive()); + Assert.assertTrue(ctx.channel().isWritable()); + Assert.assertEquals("192.168.57.1", ipAndPort.getHostName()); + + return ctx.writeAndFlush(Unpooled.wrappedBuffer(message)); + } + }; + EmbeddedChannel chDeny = newEmbeddedInetChannel("192.168.57.1", denyHandler); + ByteBuf out = chDeny.readOutbound(); + Assert.assertEquals(7, out.readableBytes()); + for (byte i = 1; i <= 7; i++) { + Assert.assertEquals(i, out.readByte()); + } + Assert.assertFalse(chDeny.isActive()); + Assert.assertFalse(chDeny.isOpen()); + + IpFilterRuleHandler allowHandler = new IpFilterRuleHandler(filter0) { + @Override + protected ChannelFuture rejected(ChannelHandlerContext ctx, InetSocketAddress ipAndPort) { + Assert.fail(); + return null; + } + }; + EmbeddedChannel chAllow = newEmbeddedInetChannel("192.168.57.2", allowHandler); + Assert.assertTrue(chAllow.isActive()); + Assert.assertTrue(chAllow.isOpen()); + } + + @Test + public void testUniqueIpFilterHandler() { + UniqueIpFilterHandler handler = new UniqueIpFilterHandler(); + + EmbeddedChannel ch1 = newEmbeddedInetChannel("91.92.93.1", handler); + Assert.assertTrue(ch1.isActive()); + EmbeddedChannel ch2 = newEmbeddedInetChannel("91.92.93.2", handler); + Assert.assertTrue(ch2.isActive()); + EmbeddedChannel ch3 = newEmbeddedInetChannel("91.92.93.1", handler); + Assert.assertFalse(ch3.isActive()); + + // false means that no data is left to read/write + Assert.assertFalse(ch1.finish()); + + EmbeddedChannel ch4 = newEmbeddedInetChannel("91.92.93.1", handler); + Assert.assertTrue(ch4.isActive()); + } + + private static EmbeddedChannel newEmbeddedInetChannel(final String ipAddress, ChannelHandler... handlers) { + return new EmbeddedChannel(handlers) { + @Override + protected SocketAddress remoteAddress0() { + return isActive()? new InetSocketAddress(ipAddress, 5421) : null; + } + }; + } + + private static InetSocketAddress newSockAddress(String ipAddress) { + return new InetSocketAddress(ipAddress, 1234); + } +}