Skip to content

Commit 5dc7be8

Browse files
committed
[auth] timeout based throttling at 10 requests per second
1 parent e68dbdf commit 5dc7be8

File tree

3 files changed

+113
-22
lines changed

3 files changed

+113
-22
lines changed

apps/boruta_web/lib/boruta_web/plugs/rate_limit.ex

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ defmodule BorutaWeb.Plugs.RateLimit do
22
@moduledoc false
33

44
defmodule Counter do
5+
@moduledoc false
56
use Agent
67

78
@base_unit :millisecond
89

10+
@memory_length 50
11+
912
@time_unit_stamps [
1013
millisecond: 1,
1114
second: 1_000,
@@ -19,19 +22,51 @@ defmodule BorutaWeb.Plugs.RateLimit do
1922
def get(ip, time_unit) do
2023
Agent.get(__MODULE__, fn counter ->
2124
Map.get(counter, ip, [])
25+
end)
26+
|> Enum.count(fn timestamp ->
27+
timestamp > :os.system_time(@base_unit) - @time_unit_stamps[time_unit]
28+
end)
29+
end
30+
31+
def throttling_timeout(ip, count, time_unit, penality) do
32+
now = :os.system_time(@base_unit)
33+
34+
request_rates = Agent.get(__MODULE__, fn counter ->
35+
Map.get(counter, ip, [])
2236
|> Enum.filter(fn timestamp ->
23-
timestamp > :os.system_time(@base_unit) - @time_unit_stamps[time_unit]
37+
timestamp > now - @memory_length * @time_unit_stamps[time_unit]
2438
end)
25-
|> Enum.count()
2639
end)
40+
|> Enum.group_by(fn timestamp ->
41+
div(timestamp, @time_unit_stamps[time_unit])
42+
end)
43+
44+
timeout = Enum.map(0..@memory_length - 1, fn i ->
45+
current = floor(now - (i * @time_unit_stamps[time_unit]))
46+
47+
Map.get(request_rates, div(current, @time_unit_stamps[time_unit]), [])
48+
end)
49+
|> Enum.reverse()
50+
|> Enum.map(fn
51+
[] -> count / @time_unit_stamps[time_unit]
52+
timestamps -> Enum.count(timestamps) / @time_unit_stamps[time_unit]
53+
end)
54+
|> Enum.reduce(1, fn factor, acc ->
55+
acc * factor * (@time_unit_stamps[time_unit] / count)
56+
end)
57+
58+
case timeout <= 1 do
59+
true -> 0
60+
false -> floor(timeout * penality)
61+
end
2762
end
2863

2964
def increment(ip, time_unit) do
3065
Agent.update(__MODULE__, fn counter ->
3166
timestamps =
3267
Map.get(counter, ip, [])
3368
|> Enum.filter(fn timestamp ->
34-
timestamp > :os.system_time(@base_unit) - @time_unit_stamps[time_unit]
69+
timestamp > :os.system_time(@base_unit) - @memory_length * @time_unit_stamps[time_unit]
3570
end)
3671

3772
Map.put(
@@ -52,11 +87,18 @@ defmodule BorutaWeb.Plugs.RateLimit do
5287

5388
Counter.increment(remote_ip, options[:time_unit])
5489

55-
case Counter.get(remote_ip, options[:time_unit]) > options[:count] do
56-
false ->
57-
conn
90+
max_timeout = options[:timeout]
5891

59-
true ->
92+
case Counter.throttling_timeout(
93+
remote_ip,
94+
options[:count],
95+
options[:time_unit],
96+
options[:penality]
97+
) do
98+
timeout when timeout < max_timeout ->
99+
:timer.sleep(timeout)
100+
conn
101+
_ ->
60102
send_resp(conn, 429, "")
61103
end
62104
end

apps/boruta_web/lib/boruta_web/router.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ defmodule BorutaWeb.Router do
22
use BorutaWeb, :router
33
use Plug.ErrorHandler
44

5+
alias BorutaWeb.Plugs.RateLimit
6+
57
import BorutaIdentityWeb.Sessions,
68
only: [
79
fetch_current_user: 2
@@ -25,6 +27,7 @@ defmodule BorutaWeb.Router do
2527

2628
pipeline :api do
2729
plug(:accepts, ["json", "jwt"])
30+
plug RateLimit, count: 10, time_unit: :second, penality: 500, timeout: 5_000
2831
end
2932

3033
scope "/", BorutaWeb do

apps/boruta_web/test/boruta_web/plugs/rate_limit_test.exs

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,9 @@ defmodule BorutaWeb.Plugs.RateLimitTest do
1717
test "request is throttled", %{conn: conn} do
1818
:timer.sleep(1000)
1919
b_conn = %{conn | remote_ip: {127, 0, 0, 2}}
20-
options = [time_unit: :second, count: 5]
20+
options = [time_unit: :second, count: 5, penality: 500]
2121
assert RateLimit.call(conn, options) == conn
2222
assert RateLimit.call(b_conn, options) == b_conn
23-
assert RateLimit.call(conn, options) == conn
24-
assert RateLimit.call(b_conn, options) == b_conn
25-
assert RateLimit.call(conn, options) == conn
26-
assert RateLimit.call(b_conn, options) == b_conn
27-
assert RateLimit.call(conn, options) == conn
28-
assert RateLimit.call(b_conn, options) == b_conn
29-
assert RateLimit.call(conn, options) == conn
30-
assert RateLimit.call(b_conn, options) == b_conn
31-
assert RateLimit.call(conn, options).status == 429
32-
assert RateLimit.call(b_conn, options).status == 429
3323
end
3424
end
3525

@@ -57,32 +47,88 @@ defmodule BorutaWeb.Plugs.RateLimitTest do
5747
end
5848
end
5949

50+
describe "Counter.throttling_timeout" do
51+
test "gives the timeout within the time unit range" do
52+
:timer.sleep(1000)
53+
ip = :ip
54+
time_unit = :second
55+
penality = 100
56+
count = 1
57+
58+
Agent.update(RateLimit.Counter, fn _counter -> %{} end)
59+
assert RateLimit.Counter.throttling_timeout(ip, count, time_unit, penality) == 0
60+
61+
Agent.update(RateLimit.Counter, fn _counter -> %{ip => [:os.system_time(:millisecond)]} end)
62+
assert RateLimit.Counter.throttling_timeout(ip, count, time_unit, penality) == 0
63+
64+
Agent.update(RateLimit.Counter, fn _counter ->
65+
%{
66+
ip => [
67+
:os.system_time(:millisecond),
68+
:os.system_time(:millisecond),
69+
:os.system_time(:millisecond),
70+
:os.system_time(:millisecond),
71+
:os.system_time(:millisecond),
72+
:os.system_time(:millisecond),
73+
:os.system_time(:millisecond)
74+
]
75+
}
76+
end)
77+
78+
assert RateLimit.Counter.throttling_timeout(ip, count, time_unit, penality) == 700
79+
80+
Agent.update(RateLimit.Counter, fn _counter ->
81+
%{
82+
ip => [
83+
:os.system_time(:millisecond) - 1000,
84+
:os.system_time(:millisecond) - 800,
85+
:os.system_time(:millisecond)
86+
]
87+
}
88+
end)
89+
90+
assert RateLimit.Counter.throttling_timeout(ip, count, time_unit, penality) == 200
91+
92+
Agent.update(RateLimit.Counter, fn _counter ->
93+
%{ip => [:os.system_time(:millisecond), :os.system_time(:millisecond) - 1000]}
94+
end)
95+
96+
assert RateLimit.Counter.throttling_timeout(ip, count, time_unit, penality) == 0
97+
end
98+
end
99+
60100
describe "Counter.increment" do
61101
test "updates the counter" do
62102
:timer.sleep(1000)
63103
ip = :ip
64104
time_unit = :second
65105

66106
assert Agent.get(RateLimit.Counter, fn counter ->
67-
Map.get(counter, ip, []) |> Enum.count()
107+
Map.get(counter, ip, [])
108+
|> Enum.count(fn timestamp -> timestamp > :os.system_time(:millisecond) - 1000 end)
68109
end) == 0
69110

70111
RateLimit.Counter.increment(ip, time_unit)
71112

72113
assert Agent.get(RateLimit.Counter, fn %{^ip => timestamps} ->
73-
Enum.count(timestamps)
114+
timestamps
115+
|> Enum.count(fn timestamp -> timestamp > :os.system_time(:millisecond) - 1000 end)
74116
end) == 1
75117

76118
RateLimit.Counter.increment(ip, time_unit)
77119

78120
assert Agent.get(RateLimit.Counter, fn %{^ip => timestamps} ->
79-
Enum.count(timestamps)
121+
timestamps
122+
|> Enum.count(fn timestamp -> timestamp > :os.system_time(:millisecond) - 1000 end)
80123
end) == 2
81124

82125
:timer.sleep(1000)
83126
RateLimit.Counter.increment(ip, time_unit)
84127

85-
assert Agent.get(RateLimit.Counter, fn %{^ip => timestamps} -> Enum.count(timestamps) end) ==
128+
assert Agent.get(RateLimit.Counter, fn %{^ip => timestamps} ->
129+
timestamps
130+
|> Enum.count(fn timestamp -> timestamp > :os.system_time(:millisecond) - 1000 end)
131+
end) ==
86132
1
87133
end
88134
end

0 commit comments

Comments
 (0)