Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ETCM-331] Add Rate Limit for rpc requests #806

Merged
merged 11 commits into from
Dec 1, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import akka.actor.ActorSystem
import io.iohk.ethereum.faucet.{FaucetConfigBuilder, FaucetSupervisor}
import io.iohk.ethereum.jsonrpc.server.controllers.ApisBase
import io.iohk.ethereum.jsonrpc.server.controllers.JsonRpcBaseController.JsonRpcConfig
import io.iohk.ethereum.jsonrpc.server.http.JsonRpcHttpServer
import io.iohk.ethereum.jsonrpc.server.http.FaucetJsonRpcHttpServer
import io.iohk.ethereum.keystore.KeyStoreImpl
import io.iohk.ethereum.mallet.service.RpcClient
import io.iohk.ethereum.utils.{ConfigUtils, KeyStoreConfig, Logger}
Expand Down Expand Up @@ -79,7 +79,7 @@ trait FaucetJsonRpcHttpServerBuilder {
with FaucetJsonRpcHealthCheckBuilder
with FaucetJsonRpcControllerBuilder =>

val faucetJsonRpcHttpServer = JsonRpcHttpServer(
val faucetJsonRpcHttpServer = FaucetJsonRpcHttpServer(
faucetJsonRpcController,
faucetJsonRpcHealthCheck,
jsonRpcConfig.httpServerConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import io.iohk.ethereum.utils.Logger
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Failure, Success}

class BasicJsonRpcHttpServer(
abstract class BasicJsonRpcHttpServer(
biandratti marked this conversation as resolved.
Show resolved Hide resolved
val jsonRpcController: JsonRpcBaseController,
val jsonRpcHealthChecker: JsonRpcHealthChecker,
config: JsonRpcHttpServerConfig
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package io.iohk.ethereum.jsonrpc.server.http

import java.security.SecureRandom
import java.time.Clock

import akka.actor.ActorSystem
import akka.http.scaladsl.model.{RemoteAddress, StatusCodes}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.{Route, StandardRoute}
import ch.megard.akka.http.cors.scaladsl.CorsDirectives.cors
import com.twitter.util.LruMap
import io.iohk.ethereum.faucet.jsonrpc.FaucetJsonRpcController
import io.iohk.ethereum.jsonrpc.server.controllers.JsonRpcBaseController
import io.iohk.ethereum.jsonrpc.server.http.JsonRpcHttpServer.JsonRpcHttpServerConfig
import io.iohk.ethereum.jsonrpc.{JsonRpcHealthChecker, JsonRpcRequest}
import io.iohk.ethereum.utils.Logger
import monix.execution.Scheduler.Implicits.global

trait FaucetJsonRpcHttpServer extends JsonRpcHttpServer {

val minRequestInterval: Int

val latestTimestampCacheSize: Int

val latestRequestTimestamps: LruMap[RemoteAddress, Long]

val clock: Clock = Clock.systemUTC()

override def route: Route = cors(corsSettings) {
(pathEndOrSingleSlash & post) {
(extractClientIP & entity(as[JsonRpcRequest])) { (clientAddress: RemoteAddress, request: JsonRpcRequest) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it enough to check IP address only in HTTP header, or maybe we shouldn't trust them?
Maybe we have to read AttributeKeys.remoteAddress? What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HI! Thanks for the comment! I think you're right. I'll create a different task for it (since it might escape the scope of this one -> just bringing back old behavior)

handleRequest(clientAddress, request)
}
}
}

def handleRequest(clientAddr: RemoteAddress, request: JsonRpcRequest): StandardRoute = {
val timeMillis = clock.instant().toEpochMilli

if (request.method == FaucetJsonRpcController.SendFunds) {
val latestRequestTimestamp = latestRequestTimestamps.getOrElse(clientAddr, 0L)
biandratti marked this conversation as resolved.
Show resolved Hide resolved
if (latestRequestTimestamp + minRequestInterval < timeMillis) {
latestRequestTimestamps.put(clientAddr, timeMillis)
complete(jsonRpcController.handleRequest(request).runToFuture)
} else {
complete(StatusCodes.TooManyRequests)
}
} else complete(jsonRpcController.handleRequest(request).runToFuture)
}
}

object FaucetJsonRpcHttpServer extends Logger {
def apply(
jsonRpcController: JsonRpcBaseController,
jsonRpcHealthchecker: JsonRpcHealthChecker,
config: JsonRpcHttpServerConfig,
secureRandom: SecureRandom
)(implicit actorSystem: ActorSystem): Either[String, JsonRpcHttpServer] =
config.mode match {
case "http" =>
Right(new FaucetBasicJsonRpcHttpServer(jsonRpcController, jsonRpcHealthchecker, config)(actorSystem))
case "https" =>
Right(new FaucetJsonRpcHttpsServer(jsonRpcController, jsonRpcHealthchecker, config, secureRandom)(actorSystem))
case _ => Left(s"Cannot start JSON RPC server: Invalid mode ${config.mode} selected")
}
}

class FaucetBasicJsonRpcHttpServer(
biandratti marked this conversation as resolved.
Show resolved Hide resolved
jsonRpcController: JsonRpcBaseController,
jsonRpcHealthChecker: JsonRpcHealthChecker,
config: JsonRpcHttpServerConfig
)(implicit actorSystem: ActorSystem)
extends BasicJsonRpcHttpServer(jsonRpcController, jsonRpcHealthChecker, config)(actorSystem)
with FaucetJsonRpcHttpServer {

//FIXME: these 2 variables should be retrieved from a config file
override val minRequestInterval: Int = 10000
biandratti marked this conversation as resolved.
Show resolved Hide resolved
override val latestTimestampCacheSize: Int = 1024

override val latestRequestTimestamps = new LruMap[RemoteAddress, Long](latestTimestampCacheSize)
}

class FaucetJsonRpcHttpsServer(
biandratti marked this conversation as resolved.
Show resolved Hide resolved
jsonRpcController: JsonRpcBaseController,
jsonRpcHealthChecker: JsonRpcHealthChecker,
config: JsonRpcHttpServerConfig,
secureRandom: SecureRandom
)(implicit actorSystem: ActorSystem)
extends JsonRpcHttpsServer(jsonRpcController, jsonRpcHealthChecker, config, secureRandom)(actorSystem)
with FaucetJsonRpcHttpServer {

//FIXME: these 2 variables should be retrieved from a config file
val minRequestInterval: Int = 10000
val latestTimestampCacheSize: Int = 1024

override val latestRequestTimestamps = new LruMap[RemoteAddress, Long](latestTimestampCacheSize)
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
package io.iohk.ethereum.jsonrpc.server.http

import java.security.SecureRandom

import akka.actor.ActorSystem
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Directives.complete
import akka.http.scaladsl.server._
import ch.megard.akka.http.cors.javadsl.CorsRejection
import ch.megard.akka.http.cors.scaladsl.CorsDirectives._
import ch.megard.akka.http.cors.scaladsl.model.HttpOriginMatcher
import ch.megard.akka.http.cors.scaladsl.settings.CorsSettings
import de.heikoseeberger.akkahttpjson4s.Json4sSupport
import io.iohk.ethereum.jsonrpc._
import io.iohk.ethereum.jsonrpc.serialization.JsonSerializers
import io.iohk.ethereum.jsonrpc.server.controllers.JsonRpcBaseController
import io.iohk.ethereum.utils.{ConfigUtils, Logger}
import monix.eval.Task
import monix.execution.Scheduler.Implicits.global
import org.json4s.{DefaultFormats, JInt, native}

trait JsonRpcHttpServer extends Json4sSupport {
abstract class JsonRpcHttpServer extends Json4sSupport {
val jsonRpcController: JsonRpcBaseController
val jsonRpcHealthChecker: JsonRpcHealthChecker

Expand All @@ -44,69 +38,17 @@ trait JsonRpcHttpServer extends Json4sSupport {
}
.result()

val route: Route = cors(corsSettings) {
(path("healthcheck") & pathEndOrSingleSlash & get) {
handleHealthcheck()
} ~ (pathEndOrSingleSlash & post) {
entity(as[JsonRpcRequest]) { request =>
handleRequest(request)
} ~ entity(as[Seq[JsonRpcRequest]]) { request =>
handleBatchRequest(request)
}
}
}
def route: Route

/**
* Try to start JSON RPC server
*/
def run(): Unit

private def handleHealthcheck(): StandardRoute = {
val responseF = jsonRpcHealthChecker.healthCheck()
val httpResponseF =
responseF.map {
case response if response.isOK =>
HttpResponse(
status = StatusCodes.OK,
entity = HttpEntity(ContentTypes.`application/json`, serialization.writePretty(response))
)
case response =>
HttpResponse(
status = StatusCodes.InternalServerError,
entity = HttpEntity(ContentTypes.`application/json`, serialization.writePretty(response))
)
}
complete(httpResponseF)
}

private def handleRequest(request: JsonRpcRequest) = {
complete(jsonRpcController.handleRequest(request).runToFuture)
}

private def handleBatchRequest(requests: Seq[JsonRpcRequest]) = {
complete {
Task
.traverse(requests)(request => jsonRpcController.handleRequest(request))
.runToFuture
}
}
}

object JsonRpcHttpServer extends Logger {

def apply(
jsonRpcController: JsonRpcBaseController,
jsonRpcHealthchecker: JsonRpcHealthChecker,
config: JsonRpcHttpServerConfig,
secureRandom: SecureRandom
)(implicit actorSystem: ActorSystem): Either[String, JsonRpcHttpServer] =
config.mode match {
case "http" => Right(new BasicJsonRpcHttpServer(jsonRpcController, jsonRpcHealthchecker, config)(actorSystem))
case "https" =>
Right(new JsonRpcHttpsServer(jsonRpcController, jsonRpcHealthchecker, config, secureRandom)(actorSystem))
case _ => Left(s"Cannot start JSON RPC server: Invalid mode ${config.mode} selected")
}

trait JsonRpcHttpServerConfig {
val mode: String
val enabled: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import io.iohk.ethereum.jsonrpc.server.http.JsonRpcHttpsServer.HttpsSetupResult
import io.iohk.ethereum.utils.Logger
import java.io.{File, FileInputStream}
import java.security.{KeyStore, SecureRandom}

import io.iohk.ethereum.jsonrpc.server.controllers.JsonRpcBaseController

import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory}

import scala.concurrent.ExecutionContext.Implicits.global
import scala.io.Source
import scala.util.{Failure, Success, Try}

class JsonRpcHttpsServer(
abstract class JsonRpcHttpsServer(
val jsonRpcController: JsonRpcBaseController,
val jsonRpcHealthChecker: JsonRpcHealthChecker,
config: JsonRpcHttpServerConfig,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.iohk.ethereum.jsonrpc.server.http

import java.security.SecureRandom

import akka.actor.ActorSystem
import akka.http.scaladsl.model.{ContentTypes, HttpEntity, HttpResponse, StatusCodes}
import akka.http.scaladsl.server.{Route, StandardRoute}
import io.iohk.ethereum.jsonrpc.JsonRpcHealthChecker
import io.iohk.ethereum.jsonrpc.server.http.JsonRpcHttpServer.JsonRpcHttpServerConfig
import ch.megard.akka.http.cors.scaladsl.CorsDirectives.cors
import akka.http.scaladsl.server.Directives._
import io.iohk.ethereum.jsonrpc._
import io.iohk.ethereum.jsonrpc.server.controllers.JsonRpcBaseController
import io.iohk.ethereum.utils.Logger
import monix.eval.Task
import monix.execution.Scheduler.Implicits.global

trait NodeJsonRpcHttpServer extends JsonRpcHttpServer {

private def handleHealthcheck(): StandardRoute = {
val responseF = jsonRpcHealthChecker.healthCheck()
val httpResponseF =
responseF.map {
case response if response.isOK =>
HttpResponse(
status = StatusCodes.OK,
entity = HttpEntity(ContentTypes.`application/json`, serialization.writePretty(response))
)
case response =>
HttpResponse(
status = StatusCodes.InternalServerError,
entity = HttpEntity(ContentTypes.`application/json`, serialization.writePretty(response))
)
}
complete(httpResponseF)
}

private def handleRequest(request: JsonRpcRequest) = {
complete(jsonRpcController.handleRequest(request).runToFuture)
}

private def handleBatchRequest(requests: Seq[JsonRpcRequest]) = {
complete {
Task
.traverse(requests)(request => jsonRpcController.handleRequest(request))
.runToFuture
}
}

override def route: Route = cors(corsSettings) {
(path("healthcheck") & pathEndOrSingleSlash & get) {
handleHealthcheck()
} ~ (pathEndOrSingleSlash & post) {
entity(as[JsonRpcRequest]) { request: JsonRpcRequest => handleRequest(request) } ~ entity(
as[Seq[JsonRpcRequest]]
) { request =>
handleBatchRequest(request)
}
}
}

}

object NodeJsonRpcHttpServer extends Logger {
def apply(
jsonRpcController: JsonRpcBaseController,
jsonRpcHealthchecker: JsonRpcHealthChecker,
config: JsonRpcHttpServerConfig,
secureRandom: SecureRandom
)(implicit actorSystem: ActorSystem): Either[String, JsonRpcHttpServer] =
config.mode match {
case "http" => Right(new NodeBasicJsonRpcHttpServer(jsonRpcController, jsonRpcHealthchecker, config)(actorSystem))
case "https" =>
Right(new NodeJsonRpcHttpsServer(jsonRpcController, jsonRpcHealthchecker, config, secureRandom)(actorSystem))
case _ => Left(s"Cannot start JSON RPC server: Invalid mode ${config.mode} selected")
}

}

class NodeBasicJsonRpcHttpServer(
biandratti marked this conversation as resolved.
Show resolved Hide resolved
jsonRpcController: JsonRpcBaseController,
jsonRpcHealthChecker: JsonRpcHealthChecker,
config: JsonRpcHttpServerConfig
)(implicit actorSystem: ActorSystem)
extends BasicJsonRpcHttpServer(jsonRpcController, jsonRpcHealthChecker, config)(actorSystem)
with NodeJsonRpcHttpServer {}

class NodeJsonRpcHttpsServer(
biandratti marked this conversation as resolved.
Show resolved Hide resolved
jsonRpcController: JsonRpcBaseController,
jsonRpcHealthChecker: JsonRpcHealthChecker,
config: JsonRpcHttpServerConfig,
secureRandom: SecureRandom
)(implicit actorSystem: ActorSystem)
extends JsonRpcHttpsServer(jsonRpcController, jsonRpcHealthChecker, config, secureRandom)(actorSystem)
with NodeJsonRpcHttpServer {}
8 changes: 2 additions & 6 deletions src/main/scala/io/iohk/ethereum/nodebuilder/NodeBuilder.scala
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package io.iohk.ethereum.nodebuilder

import java.security.SecureRandom
import java.util.concurrent.atomic.AtomicReference

import akka.actor.{ActorRef, ActorSystem}
import io.iohk.ethereum.blockchain.data.GenesisDataLoader
import io.iohk.ethereum.blockchain.sync.{BlockchainHostActor, SyncController}
import io.iohk.ethereum.consensus._
import io.iohk.ethereum.consensus.blocks.CheckpointBlockGenerator
import io.iohk.ethereum.db.components.Storages.PruningModeComponent
import io.iohk.ethereum.db.components._
import io.iohk.ethereum.db.storage.AppStateStorage
Expand All @@ -17,7 +13,7 @@ import io.iohk.ethereum.jsonrpc.NetService.NetServiceConfig
import io.iohk.ethereum.jsonrpc._
import io.iohk.ethereum.jsonrpc.server.controllers.ApisBase
import io.iohk.ethereum.jsonrpc.server.controllers.JsonRpcBaseController.JsonRpcConfig
import io.iohk.ethereum.jsonrpc.server.http.JsonRpcHttpServer
import io.iohk.ethereum.jsonrpc.server.http.NodeJsonRpcHttpServer
import io.iohk.ethereum.jsonrpc.server.ipc.JsonRpcIpcServer
import io.iohk.ethereum.keystore.{KeyStore, KeyStoreImpl}
import io.iohk.ethereum.ledger.Ledger.VMImpl
Expand Down Expand Up @@ -466,7 +462,7 @@ trait JSONRpcHttpServerBuilder {
with JSONRpcConfigBuilder =>

lazy val maybeJsonRpcHttpServer =
JsonRpcHttpServer(jsonRpcController, jsonRpcHealthChecker, jsonRpcConfig.httpServerConfig, secureRandom)
NodeJsonRpcHttpServer(jsonRpcController, jsonRpcHealthChecker, jsonRpcConfig.httpServerConfig, secureRandom)
}

trait JSONRpcIpcServerBuilder {
Expand Down