1
+ package de .upb .cs .swt .delphi .webapi
2
+
3
+
4
+ import akka .actor .{Actor , ActorLogging , ActorRef , Props }
5
+ import akka .actor .Timers
6
+ import akka .http .scaladsl .model .RemoteAddress
7
+ import de .upb .cs .swt .delphi .webapi .ElasticRequestLimiter ._
8
+ import de .upb .cs .swt .delphi .webapi .ElasticActorManager .ElasticMessage
9
+
10
+ import scala .concurrent .duration ._
11
+ import scala .collection .mutable
12
+
13
+ // Limits the number of requests any given IP can make by tracking how many requests an IP has made within a given
14
+ // window of time, and timing out any IP that exceeds a threshold by rejecting any further request for a period of time
15
+ class ElasticRequestLimiter (configuration : Configuration , nextActor : ActorRef ) extends Actor with ActorLogging with Timers {
16
+
17
+ private val window = 1 second
18
+ private val threshold = 10
19
+ private val timeout = 2 hours
20
+
21
+ private var recentIPs : mutable.Map [String , Int ] = mutable.Map ()
22
+ private var blockedIPs : mutable.Set [String ] = mutable.Set ()
23
+
24
+ override def preStart (): Unit = {
25
+ log.info(" Request limiter started" )
26
+ timers.startPeriodicTimer(ClearTimer , ClearLogs , window)
27
+ }
28
+ override def postStop (): Unit = log.info(" Request limiter shut down" )
29
+
30
+ override def receive = {
31
+ case Validate (rawIp, message) => {
32
+ val ip = rawIp.toOption.map(_.getHostAddress).getOrElse(" unknown" )
33
+ // First, reject IPs marked as blocked
34
+ if (blockedIPs.contains(ip)) {
35
+ rejectRequest()
36
+ } else {
37
+ // Check if this IP has made any requests recently
38
+ if (recentIPs.contains(ip)) {
39
+ // If so, increment their counter and test if they have exceeded the request threshold
40
+ recentIPs.update(ip, recentIPs(ip) + 1 )
41
+ if (recentIPs(ip) > threshold) {
42
+ // If the threshold has been exceeded, mark this IP as blocked and reject it, and set up a message to unblock it after a period
43
+ blockedIPs += ip
44
+ log.info(" Blocked IP {} due to exceeding request frequency threshold" , ip)
45
+ timers.startSingleTimer(ForgiveTimer (ip), Forgive (ip), timeout)
46
+ rejectRequest()
47
+ } else {
48
+ // Else, forward this message
49
+ nextActor forward message
50
+ }
51
+ } else {
52
+ // Else, register their request in the map and pass it to the next actor
53
+ recentIPs += (ip -> 1 )
54
+ nextActor forward message
55
+ }
56
+ }
57
+ }
58
+ case ClearLogs =>
59
+ recentIPs.clear()
60
+ case Forgive (ip) => {
61
+ blockedIPs -= ip
62
+ log.info(" Forgave IP {} after timeout" , ip)
63
+ }
64
+ }
65
+
66
+ // Rejects requests from blocked IPs
67
+ private def rejectRequest () =
68
+ sender() ! " Sorry, you have exceeded the limit on request frequency for unregistered users.\n " +
69
+ " As a result, you have been timed out.\n " +
70
+ " Please wait a while or register an account with us to continue using this service."
71
+ }
72
+
73
+ object ElasticRequestLimiter {
74
+ def props (configuration : Configuration , nextActor : ActorRef ) : Props = Props (new ElasticRequestLimiter (configuration, nextActor))
75
+
76
+ final case class Validate (rawIp : RemoteAddress , message : ElasticMessage )
77
+ final case object ClearLogs
78
+ final case class Forgive (ip : String )
79
+
80
+ final case object ClearTimer
81
+ final case class ForgiveTimer (ip : String )
82
+ }
0 commit comments