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