Merge pull request #321 from atoulme/add_ipchecks

Add support for IP range checks
diff --git a/dependency-versions.gradle b/dependency-versions.gradle
index 3875d70..6f08571 100644
--- a/dependency-versions.gradle
+++ b/dependency-versions.gradle
@@ -14,6 +14,7 @@
   dependencies {
     dependency('ch.qos.logback:logback-classic:1.2.3')
     dependency('commons-codec:commons-codec:1.14')
+    dependency('commons-net:commons-net:3.8.0')
     dependency('com.fasterxml.jackson.core:jackson-databind:2.11.0')
     dependency('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.0')
     dependency('com.github.jnr:jnr-ffi:2.1.9')
diff --git a/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCApp.kt b/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCApp.kt
index 782d644..b2ba1c7 100644
--- a/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCApp.kt
+++ b/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCApp.kt
@@ -27,6 +27,7 @@
 import org.apache.tuweni.jsonrpc.methods.MeteredHandler
 import org.apache.tuweni.jsonrpc.methods.MethodAllowListHandler
 import org.apache.tuweni.metrics.MetricsService
+import org.apache.tuweni.net.ip.IPRangeChecker
 import org.apache.tuweni.net.tls.VertxTrustOptions
 import org.bouncycastle.jce.provider.BouncyCastleProvider
 import org.slf4j.LoggerFactory
@@ -90,14 +91,15 @@
       config.basicAuthUsername(),
       config.basicAuthPassword(),
       config.basicAuthRealm(),
-      methodHandler = handler::handleRequest,
+      IPRangeChecker.create(config.allowedRanges(), config.rejectedRanges()),
       Executors.newFixedThreadPool(
         config.numberOfThreads()
       ) {
         val thread = Thread("jsonrpc")
         thread.isDaemon = true
         thread
-      }.asCoroutineDispatcher()
+      }.asCoroutineDispatcher(),
+      handler::handleRequest
     )
     Runtime.getRuntime().addShutdownHook(
       Thread {
diff --git a/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCConfig.kt b/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCConfig.kt
index 8977dd3..130efc8 100644
--- a/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCConfig.kt
+++ b/jsonrpc-app/src/main/kotlin/org/apache/tuweni/jsonrpc/app/JSONRPCConfig.kt
@@ -45,6 +45,8 @@
       .addString("basicAuthPassword", null, "HTTP Basic Auth password", null)
       .addString("basicAuthRealm", null, "HTTP Basic Auth realm", null)
       .addListOfString("allowedMethods", Collections.emptyList(), "Allowed JSON-RPC methods", null)
+      .addListOfString("allowedRanges", Collections.singletonList("0.0.0.0/0"), "Allowed IP ranges", null)
+      .addListOfString("rejectedRanges", Collections.emptyList(), "Rejected IP ranges", null)
       .toSchema()
   }
 
@@ -65,4 +67,6 @@
   fun basicAuthPassword() = config.getString("basicAuthPassword")
   fun basicAuthRealm() = config.getString("basicAuthRealm")
   fun allowedMethods() = config.getListOfString("allowedMethods")
+  fun allowedRanges() = config.getListOfString("allowedRanges")
+  fun rejectedRanges() = config.getListOfString("rejectedRanges")
 }
diff --git a/jsonrpc/build.gradle b/jsonrpc/build.gradle
index 14bdac7..e2d2da2 100644
--- a/jsonrpc/build.gradle
+++ b/jsonrpc/build.gradle
@@ -29,6 +29,7 @@
   implementation project(':crypto')
   implementation project(':concurrent')
   implementation project(':eth')
+  implementation project(':net')
   implementation project(':units')
 
   testImplementation project(':io')
diff --git a/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/JSONRPCServer.kt b/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/JSONRPCServer.kt
index 5c6ffbe..1a05e89 100644
--- a/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/JSONRPCServer.kt
+++ b/jsonrpc/src/main/kotlin/org/apache/tuweni/jsonrpc/JSONRPCServer.kt
@@ -47,6 +47,7 @@
 import org.apache.tuweni.eth.JSONRPCResponse
 import org.apache.tuweni.eth.internalError
 import org.apache.tuweni.eth.parseError
+import org.apache.tuweni.net.ip.IPRangeChecker
 import org.slf4j.LoggerFactory
 import java.io.IOException
 import java.lang.IllegalArgumentException
@@ -62,8 +63,9 @@
   val basicAuthenticationUsername: String? = null,
   val basicAuthenticationPassword: String? = null,
   val basicAuthRealm: String = "Apache Tuweni JSON-RPC proxy",
-  val methodHandler: (JSONRPCRequest) -> JSONRPCResponse,
+  val ipRangeChecker: IPRangeChecker = IPRangeChecker.allowAll(),
   override val coroutineContext: CoroutineContext = Dispatchers.Default,
+  val methodHandler: (JSONRPCRequest) -> JSONRPCResponse,
 ) : CoroutineScope {
 
   companion object {
@@ -94,6 +96,13 @@
       serverOptions.setTrustOptions(it)
     }
     httpServer = vertx.createHttpServer()
+    httpServer?.connectionHandler {
+      val remoteAddress = it.remoteAddress().hostAddress()
+      if (!ipRangeChecker.check(remoteAddress)) {
+        logger.debug("Rejecting IP {}", remoteAddress)
+        it.close()
+      }
+    }
     httpServer?.exceptionHandler {
       logger.error(it.message, it)
     }
diff --git a/net/build.gradle b/net/build.gradle
index e419957..f63a270 100644
--- a/net/build.gradle
+++ b/net/build.gradle
@@ -17,6 +17,7 @@
   implementation project(':crypto')
   implementation project(':io')
   implementation 'com.google.guava:guava'
+  implementation 'commons-net:commons-net'
   compileOnly 'io.vertx:vertx-core'
   compileOnly 'org.bouncycastle:bcprov-jdk15on'
   compileOnly 'org.bouncycastle:bcpkix-jdk15on'
diff --git a/net/src/main/java/org/apache/tuweni/net/ip/IPRangeChecker.java b/net/src/main/java/org/apache/tuweni/net/ip/IPRangeChecker.java
new file mode 100644
index 0000000..8ef4b9f
--- /dev/null
+++ b/net/src/main/java/org/apache/tuweni/net/ip/IPRangeChecker.java
@@ -0,0 +1,84 @@
+/*
+ * 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.tuweni.net.ip;
+
+import org.apache.commons.net.util.SubnetUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Checks that an IP is allowed according to IP ranges.
+ */
+public class IPRangeChecker {
+
+  /**
+   * Creates a new IP range checker.
+   * 
+   * @param allowedRanges list of allowed ranges.
+   * @param rejectedRanges list of rejected ranges
+   * @return a new IP range checker
+   * @throws IllegalArgumentException if a range is invalid.
+   */
+  public static IPRangeChecker create(List<String> allowedRanges, List<String> rejectedRanges) {
+    List<SubnetUtils> allowed = new ArrayList<>();
+    List<SubnetUtils> rejected = new ArrayList<>();
+    for (String iprange : allowedRanges) {
+      allowed.add(new SubnetUtils(iprange));
+    }
+    for (String iprange : rejectedRanges) {
+      rejected.add(new SubnetUtils(iprange));
+    }
+    return new IPRangeChecker(allowed, rejected);
+  }
+
+  /**
+   * Creates a checker that allows any IP.
+   * 
+   * @return a new range checker that allows any IP.
+   */
+  public static IPRangeChecker allowAll() {
+    return create(Collections.singletonList("0.0.0.0/0"), Collections.emptyList());
+  }
+
+  private final List<SubnetUtils> allowedRanges;
+  private final List<SubnetUtils> rejectedRanges;
+
+  private IPRangeChecker(List<SubnetUtils> allowedRanges, List<SubnetUtils> rejectedRanges) {
+    this.allowedRanges = allowedRanges;
+    this.rejectedRanges = rejectedRanges;
+  }
+
+  /**
+   * Checks if an IP address is inside the ranges of this checker
+   * 
+   * @param ip the IP address to check
+   * @return true if it is inside the ranges of the checker.
+   */
+  public boolean check(String ip) {
+    for (SubnetUtils subnetUtils : allowedRanges) {
+      if (!subnetUtils.getInfo().isInRange(ip)) {
+        return false;
+      }
+    }
+    for (SubnetUtils subnetUtils : rejectedRanges) {
+      if (subnetUtils.getInfo().isInRange(ip)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+
+}
diff --git a/net/src/test/java/org/apache/tuweni/net/ip/IPRangerCheckerTest.java b/net/src/test/java/org/apache/tuweni/net/ip/IPRangerCheckerTest.java
new file mode 100644
index 0000000..3658e4a
--- /dev/null
+++ b/net/src/test/java/org/apache/tuweni/net/ip/IPRangerCheckerTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.tuweni.net.ip;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Collections;
+
+import org.junit.jupiter.api.Test;
+
+class IPRangerCheckerTest {
+
+  @Test
+  void testAllowedAll() {
+    IPRangeChecker checker = IPRangeChecker.allowAll();
+    assertTrue(checker.check("127.0.0.1"));
+    assertTrue(checker.check("192.168.0.1"));
+    assertTrue(checker.check("10.0.10.1"));
+  }
+
+  @Test
+  void testRejectRange() {
+    IPRangeChecker checker =
+        IPRangeChecker.create(Collections.singletonList("0.0.0.0/0"), Collections.singletonList("10.0.0.0/24"));
+    assertTrue(checker.check("127.0.0.1"));
+    assertTrue(checker.check("192.168.0.1"));
+    assertFalse(checker.check("10.0.0.2"));
+  }
+}