Skip to content

Commit ac6f7e7

Browse files
committed
[ETCM-331] Separate RateLimit validation in a different trait
1 parent df265cb commit ac6f7e7

File tree

5 files changed

+104
-67
lines changed

5 files changed

+104
-67
lines changed

src/main/resources/application.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ mantis {
202202
# any domain.
203203
cors-allowed-origins = []
204204

205-
# Ip Limit tracking for JSON-RPC requests
205+
# Rate Limit for JSON-RPC requests
206206
# Limits the amount of request the same ip can perform in a given amount of time
207207
rate-limit {
208208
# If enabled, restrictions are applied

src/main/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServer.scala

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package io.iohk.ethereum.jsonrpc.server.http
22

33
import java.security.SecureRandom
4-
import java.time.Clock
54

65
import akka.actor.ActorSystem
76
import akka.http.scaladsl.model._
@@ -11,7 +10,6 @@ import ch.megard.akka.http.cors.javadsl.CorsRejection
1110
import ch.megard.akka.http.cors.scaladsl.CorsDirectives._
1211
import ch.megard.akka.http.cors.scaladsl.model.HttpOriginMatcher
1312
import ch.megard.akka.http.cors.scaladsl.settings.CorsSettings
14-
import com.twitter.util.LruMap
1513
import de.heikoseeberger.akkahttpjson4s.Json4sSupport
1614
import io.iohk.ethereum.faucet.jsonrpc.FaucetJsonRpcController
1715
import io.iohk.ethereum.jsonrpc._
@@ -28,7 +26,7 @@ import com.typesafe.config.{Config => TypesafeConfig}
2826

2927
import scala.concurrent.duration.{FiniteDuration, _}
3028

31-
trait JsonRpcHttpServer extends Json4sSupport {
29+
trait JsonRpcHttpServer extends Json4sSupport with RateLimit {
3230
val jsonRpcController: JsonRpcBaseController
3331
val jsonRpcHealthChecker: JsonRpcHealthChecker
3432
val config: JsonRpcHttpServerConfig
@@ -39,10 +37,6 @@ trait JsonRpcHttpServer extends Json4sSupport {
3937

4038
def corsAllowedOrigins: HttpOriginMatcher
4139

42-
val latestRequestTimestamps = new LruMap[RemoteAddress, Long](config.rateLimit.latestTimestampCacheSize)
43-
44-
val clock: Clock = Clock.systemUTC()
45-
4640
val corsSettings = CorsSettings.defaultSettings
4741
.withAllowGenericHttpRequests(true)
4842
.withAllowedOrigins(corsAllowedOrigins)
@@ -71,17 +65,16 @@ trait JsonRpcHttpServer extends Json4sSupport {
7165
}
7266

7367
def handleRequest(clientAddress: RemoteAddress, request: JsonRpcRequest): StandardRoute = {
68+
//FIXME: FaucetJsonRpcController.Status should be part of a Healthcheck request or alike.
69+
// As a temporary solution, it is being excluded from the Rate Limit.
7470
if (config.rateLimit.enabled && request.method != FaucetJsonRpcController.Status) {
75-
handleRestrictedRequest(clientAddress, request)
71+
handleRateLimitedRequest(clientAddress, request)
7672
} else complete(jsonRpcController.handleRequest(request).runToFuture)
7773
}
7874

79-
def handleRestrictedRequest(clientAddress: RemoteAddress, request: JsonRpcRequest): StandardRoute = {
80-
val timeMillis = clock.instant().toEpochMilli
81-
val latestRequestTimestamp = latestRequestTimestamps.getOrElse(clientAddress, 0L)
82-
83-
if (latestRequestTimestamp + config.rateLimit.minRequestInterval.toMillis < timeMillis) {
84-
latestRequestTimestamps.put(clientAddress, timeMillis)
75+
def handleRateLimitedRequest(clientAddress: RemoteAddress, request: JsonRpcRequest): StandardRoute = {
76+
if (isRequestAvailable(clientAddress)) {
77+
log.warn(s"Request limit exceeded for ip 1 ${clientAddress.toIP.getOrElse("unknown")}")
8578
complete(jsonRpcController.handleRequest(request).runToFuture)
8679
} else complete(StatusCodes.TooManyRequests)
8780
}
@@ -144,15 +137,15 @@ object JsonRpcHttpServer extends Logger {
144137
case _ => Left(s"Cannot start JSON RPC server: Invalid mode ${config.mode} selected")
145138
}
146139

147-
trait RateLimit {
140+
trait RateLimitConfig {
148141
val enabled: Boolean
149142
val minRequestInterval: FiniteDuration
150143
val latestTimestampCacheSize: Int
151144
}
152145

153-
object RateLimit {
154-
def apply(rateLimitConfig: TypesafeConfig): RateLimit =
155-
new RateLimit {
146+
object RateLimitConfig {
147+
def apply(rateLimitConfig: TypesafeConfig): RateLimitConfig =
148+
new RateLimitConfig {
156149
override val enabled: Boolean = rateLimitConfig.getBoolean("enabled")
157150
override val minRequestInterval: FiniteDuration =
158151
rateLimitConfig.getDuration("min-request-interval").toMillis.millis
@@ -166,7 +159,7 @@ object JsonRpcHttpServer extends Logger {
166159
val interface: String
167160
val port: Int
168161
val corsAllowedOrigins: HttpOriginMatcher
169-
val rateLimit: RateLimit
162+
val rateLimit: RateLimitConfig
170163
}
171164

172165
object JsonRpcHttpServerConfig {
@@ -181,7 +174,7 @@ object JsonRpcHttpServer extends Logger {
181174

182175
override val corsAllowedOrigins = ConfigUtils.parseCorsAllowedOrigins(rpcHttpConfig, "cors-allowed-origins")
183176

184-
override val rateLimit = RateLimit(rpcHttpConfig.getConfig("rate-limit"))
177+
override val rateLimit = RateLimitConfig(rpcHttpConfig.getConfig("rate-limit"))
185178
}
186179
}
187180
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.iohk.ethereum.jsonrpc.server.http
2+
3+
import java.time.Clock
4+
5+
import akka.http.scaladsl.model.RemoteAddress
6+
import com.twitter.util.LruMap
7+
import io.iohk.ethereum.jsonrpc.server.http.JsonRpcHttpServer.JsonRpcHttpServerConfig
8+
import io.iohk.ethereum.utils.Logger
9+
10+
trait RateLimit extends Logger {
11+
12+
val config: JsonRpcHttpServerConfig
13+
14+
val latestRequestTimestamps = new LruMap[RemoteAddress, Long](config.rateLimit.latestTimestampCacheSize)
15+
16+
val clock: Clock = Clock.systemUTC()
17+
18+
def isRequestAvailable(clientAddress: RemoteAddress): Boolean = {
19+
val timeMillis = clock.instant().toEpochMilli
20+
val latestRequestTimestamp = latestRequestTimestamps.getOrElse(clientAddress, 0L)
21+
22+
val response = latestRequestTimestamp + config.rateLimit.minRequestInterval.toMillis < timeMillis
23+
if (response) latestRequestTimestamps.put(clientAddress, timeMillis)
24+
response
25+
}
26+
}

src/test/scala/io/iohk/ethereum/jsonrpc/server/http/JsonRpcHttpServerSpec.scala

Lines changed: 63 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,23 @@ import java.net.InetAddress
44
import java.time.{Clock, Instant, ZoneId}
55
import java.util.concurrent.TimeUnit
66

7+
import akka.actor.ActorSystem
78
import akka.http.scaladsl.model._
89
import akka.http.scaladsl.model.headers.{HttpOrigin, Origin}
910
import akka.http.scaladsl.server.Route
1011
import akka.http.scaladsl.testkit.ScalatestRouteTest
1112
import akka.util.ByteString
1213
import ch.megard.akka.http.cors.scaladsl.model.HttpOriginMatcher
13-
import io.iohk.ethereum.jsonrpc.server.http.JsonRpcHttpServer.{JsonRpcHttpServerConfig, RateLimit}
14+
import io.iohk.ethereum.jsonrpc.server.http.JsonRpcHttpServer.{JsonRpcHttpServerConfig, RateLimitConfig}
1415
import io.iohk.ethereum.jsonrpc.{JsonRpcController, JsonRpcHealthChecker, JsonRpcResponse}
1516
import monix.eval.Task
1617
import org.json4s.JsonAST.{JInt, JString}
1718
import org.scalamock.scalatest.MockFactory
1819
import org.scalatest.flatspec.AnyFlatSpec
1920
import org.scalatest.matchers.should.Matchers
2021
import akka.http.scaladsl.model.headers._
22+
import io.iohk.ethereum.utils.Logger
23+
import io.iohk.ethereum.jsonrpc.server.controllers.JsonRpcBaseController
2124

2225
import scala.concurrent.duration.FiniteDuration
2326

@@ -108,7 +111,7 @@ class JsonRpcHttpServerSpec extends AnyFlatSpec with Matchers with ScalatestRout
108111
val postRequest =
109112
HttpRequest(HttpMethods.POST, uri = "/", entity = HttpEntity(MediaTypes.`application/json`, jsonRequest))
110113

111-
postRequest ~> Route.seal(mockJsonRpcHttpServerWithIpRestriction.route) ~> check {
114+
postRequest ~> Route.seal(mockJsonRpcHttpServerWithRateLimit.route) ~> check {
112115
status shouldEqual StatusCodes.OK
113116
responseAs[String] shouldEqual """{"jsonrpc":"2.0","result":"this is a response","id":1}"""
114117
}
@@ -123,11 +126,11 @@ class JsonRpcHttpServerSpec extends AnyFlatSpec with Matchers with ScalatestRout
123126
val postRequest =
124127
HttpRequest(HttpMethods.POST, uri = "/", entity = HttpEntity(MediaTypes.`application/json`, jsonRequest))
125128

126-
postRequest ~> Route.seal(mockJsonRpcHttpServerWithIpRestriction.route) ~> check {
129+
postRequest ~> Route.seal(mockJsonRpcHttpServerWithRateLimit.route) ~> check {
127130
status shouldEqual StatusCodes.OK
128131
responseAs[String] shouldEqual """{"jsonrpc":"2.0","result":"this is a response","id":1}"""
129132
}
130-
postRequest ~> Route.seal(mockJsonRpcHttpServerWithIpRestriction.route) ~> check {
133+
postRequest ~> Route.seal(mockJsonRpcHttpServerWithRateLimit.route) ~> check {
131134
status shouldEqual StatusCodes.TooManyRequests
132135
}
133136
}
@@ -143,7 +146,7 @@ class JsonRpcHttpServerSpec extends AnyFlatSpec with Matchers with ScalatestRout
143146
val postRequest =
144147
HttpRequest(HttpMethods.POST, uri = "/", entity = HttpEntity(MediaTypes.`application/json`, jsonRequest))
145148

146-
postRequest ~> Route.seal(mockJsonRpcHttpServerWithIpRestriction.route) ~> check {
149+
postRequest ~> Route.seal(mockJsonRpcHttpServerWithRateLimit.route) ~> check {
147150
status === StatusCodes.MethodNotAllowed
148151
}
149152
}
@@ -158,17 +161,17 @@ class JsonRpcHttpServerSpec extends AnyFlatSpec with Matchers with ScalatestRout
158161
val postRequest =
159162
HttpRequest(HttpMethods.POST, uri = "/", entity = HttpEntity(MediaTypes.`application/json`, jsonRequest))
160163

161-
postRequest ~> Route.seal(mockJsonRpcHttpServerWithIpRestriction.route) ~> check {
164+
postRequest ~> Route.seal(mockJsonRpcHttpServerWithRateLimit.route) ~> check {
162165
status shouldEqual StatusCodes.OK
163166
responseAs[String] shouldEqual """{"jsonrpc":"2.0","result":"this is a response","id":1}"""
164167
}
165-
postRequest ~> Route.seal(mockJsonRpcHttpServerWithIpRestriction.route) ~> check {
168+
postRequest ~> Route.seal(mockJsonRpcHttpServerWithRateLimit.route) ~> check {
166169
status shouldEqual StatusCodes.TooManyRequests
167170
}
168171

169-
fakeClock.advanceTime(10)
172+
fakeClock.advanceTime(2 * serverConfigWithRateLimit.rateLimit.minRequestInterval.toMillis)
170173

171-
postRequest ~> Route.seal(mockJsonRpcHttpServerWithIpRestriction.route) ~> check {
174+
postRequest ~> Route.seal(mockJsonRpcHttpServerWithRateLimit.route) ~> check {
172175
status shouldEqual StatusCodes.OK
173176
responseAs[String] shouldEqual """{"jsonrpc":"2.0","result":"this is a response","id":1}"""
174177
}
@@ -192,18 +195,18 @@ class JsonRpcHttpServerSpec extends AnyFlatSpec with Matchers with ScalatestRout
192195
jsonRequest)
193196
)
194197

195-
postRequest ~> Route.seal(mockJsonRpcHttpServerWithIpRestriction.route) ~> check {
198+
postRequest ~> Route.seal(mockJsonRpcHttpServerWithRateLimit.route) ~> check {
196199
status shouldEqual StatusCodes.OK
197200
responseAs[String] shouldEqual """{"jsonrpc":"2.0","result":"this is a response","id":1}"""
198201
}
199-
postRequest2 ~> Route.seal(mockJsonRpcHttpServerWithIpRestriction.route) ~> check {
202+
postRequest2 ~> Route.seal(mockJsonRpcHttpServerWithRateLimit.route) ~> check {
200203
status shouldEqual StatusCodes.OK
201204
responseAs[String] shouldEqual """{"jsonrpc":"2.0","result":"this is a response","id":1}"""
202205
}
203206
}
204207

205208
trait TestSetup extends MockFactory {
206-
val rateLimitConfig = new RateLimit {
209+
val rateLimitConfig = new RateLimitConfig {
207210
override val enabled: Boolean = false
208211
override val minRequestInterval: FiniteDuration = FiniteDuration.apply(5, TimeUnit.SECONDS)
209212
override val latestTimestampCacheSize: Int = 1024
@@ -215,51 +218,66 @@ class JsonRpcHttpServerSpec extends AnyFlatSpec with Matchers with ScalatestRout
215218
override val interface: String = ""
216219
override val port: Int = 123
217220
override val corsAllowedOrigins = HttpOriginMatcher.*
218-
override val rateLimit: RateLimit = rateLimitConfig
221+
override val rateLimit: RateLimitConfig = rateLimitConfig
219222
}
220223

221-
val mockJsonRpcController = mock[JsonRpcController]
222-
val mockJsonRpcHealthChecker = mock[JsonRpcHealthChecker]
223-
val mockJsonRpcHttpServer = new JsonRpcHttpServer {
224-
override val jsonRpcController = mockJsonRpcController
225-
override val jsonRpcHealthChecker = mockJsonRpcHealthChecker
226-
override val config: JsonRpcHttpServerConfig = serverConfig
227-
228-
def run(): Unit = ()
229-
230-
override def corsAllowedOrigins: HttpOriginMatcher = config.corsAllowedOrigins
224+
val rateLimitEnabledConfig = new RateLimitConfig {
225+
override val enabled: Boolean = true
226+
override val minRequestInterval: FiniteDuration = FiniteDuration.apply(5, TimeUnit.SECONDS)
227+
override val latestTimestampCacheSize: Int = 1024
231228
}
232229

233-
val corsAllowedOrigin = HttpOrigin("http://localhost:3333")
234-
235-
val mockJsonRpcHttpServerWithCors = new JsonRpcHttpServer {
236-
override val jsonRpcController = mockJsonRpcController
237-
override val jsonRpcHealthChecker = mockJsonRpcHealthChecker
238-
override val config: JsonRpcHttpServerConfig = serverConfig
239-
240-
def run(): Unit = ()
241-
242-
override def corsAllowedOrigins: HttpOriginMatcher = HttpOriginMatcher(corsAllowedOrigin)
230+
val serverConfigWithRateLimit = new JsonRpcHttpServerConfig {
231+
override val mode: String = "mockJsonRpc"
232+
override val enabled: Boolean = true
233+
override val interface: String = ""
234+
override val port: Int = 123
235+
override val corsAllowedOrigins = HttpOriginMatcher.*
236+
override val rateLimit: RateLimitConfig = rateLimitEnabledConfig
243237
}
244238

239+
val mockJsonRpcController = mock[JsonRpcController]
240+
val mockJsonRpcHealthChecker = mock[JsonRpcHealthChecker]
245241
val fakeClock = new FakeClock
246-
val mockJsonRpcHttpServerWithIpRestriction = new JsonRpcHttpServer {
247-
override val jsonRpcController = mockJsonRpcController
248-
override val jsonRpcHealthChecker = mockJsonRpcHealthChecker
249-
override val config: JsonRpcHttpServerConfig = serverConfig
250242

251-
override val clock: Clock = fakeClock
243+
val mockJsonRpcHttpServer = new FakeJsonRpcHttpServer(
244+
mockJsonRpcController,
245+
mockJsonRpcHealthChecker,
246+
serverConfig,
247+
serverConfig.corsAllowedOrigins,
248+
fakeClock)
252249

253-
override val config.rateLimit.`enabled` = true
254-
override val config.rateLimit.minRequestInterval: FiniteDuration = config.rateLimit.minRequestInterval
255-
256-
def run(): Unit = ()
257-
258-
override def corsAllowedOrigins: HttpOriginMatcher = config.corsAllowedOrigins
259-
}
250+
val corsAllowedOrigin = HttpOrigin("http://localhost:3333")
251+
val mockJsonRpcHttpServerWithCors = new FakeJsonRpcHttpServer(
252+
mockJsonRpcController,
253+
mockJsonRpcHealthChecker,
254+
serverConfig,
255+
HttpOriginMatcher(corsAllowedOrigin),
256+
fakeClock)
257+
258+
val mockJsonRpcHttpServerWithRateLimit = new FakeJsonRpcHttpServer(
259+
mockJsonRpcController,
260+
mockJsonRpcHealthChecker,
261+
serverConfigWithRateLimit,
262+
serverConfigWithRateLimit.corsAllowedOrigins,
263+
fakeClock)
260264
}
261265
}
262266

267+
class FakeJsonRpcHttpServer(
268+
val jsonRpcController: JsonRpcBaseController,
269+
val jsonRpcHealthChecker: JsonRpcHealthChecker,
270+
val config: JsonRpcHttpServerConfig,
271+
val cors: HttpOriginMatcher,
272+
val testClock: Clock
273+
)(implicit val actorSystem: ActorSystem)
274+
extends JsonRpcHttpServer
275+
with Logger {
276+
def run(): Unit = ()
277+
override def corsAllowedOrigins: HttpOriginMatcher = cors
278+
override val clock = testClock
279+
}
280+
263281
class FakeClock extends Clock {
264282

265283
var time: Instant = Instant.now()

src/universal/conf/faucet.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ mantis {
118118
# any domain.
119119
cors-allowed-origins = []
120120

121-
# Ip Limit tracking for JSON-RPC requests
121+
# Rate Limit for JSON-RPC requests
122122
# Limits the amount of request the same ip can perform in a given amount of time
123123
rate-limit {
124124
# If enabled, restrictions are applied

0 commit comments

Comments
 (0)